Build 450 v0.9.5

## World travel / main-menu load: stale "Settings.TravelData" RAM hygiene

### Background (player-visible symptom)

- After **EA world travel** (e.g. homeworld -> Oasis Landing), saving, **exit to main menu**, then loading a **different** save (including a “fresh” save with KW defaults), KW could appear **Enabled** with **wrong toggles**, pie-menu oddities, or script errors until a second load or full game restart.
- Closing the game process cleared the issue; **main menu -> same save** without quitting often did not fully reset script static state.
- Root cause class: **in-process RAM carryover**, not a missing STBL row. The main-menu “script modified” popup, when present, is a separate native/engine signal.

### Root cause (technical)

- Legacy KW kept a **session-only** copy of the full "Settings" object in static field "Settings.__travelData" ("Settings.TravelData", "[Persistable(false)]").
- On **world stop** during EA travel, "Main.Stop" called "Settings.StashTravelDataOnWorldStop(__settings)" so homeworld settings could be reapplied on the **vacation/future** cold start ("Main.Start" when "__settings == null || PreviousVersion == 0").
- Stash was also triggered in cases that were **not** a valid active EA travel handoff (e.g. sub-world / stale "GameStates" flags), and the stash was **not** reliably cleared on **main-menu load** or **non-travel cold start**.
- On **cold settings load** ("Main.Start" “new settings” path), if "TravelData != null", KW replaced "__settings" with the stashed object and called "Apply(false)" -> effectively **injecting the previous world’s settings** into the wrong save/world context.
- "[PersistableStatic]" on other mods is similar: the main menu does **not** wipe all script static RAM; only a full process exit or a deliberate clear/deserialize path does.

### Design intent after fix

- **Valid EA travel transition** (homeworld -> vacation/future, while "GameStates" reports active travel data): may still stash and consume "TravelData" once on the destination cold start (preserves legacy “bring homeworld KW settings to the trip” behavior for the "Settings" blob).
- **Everything else** (main menu, load another save, stale travel flags): **discard** stash; never apply homeworld "TravelData" to the new load.

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| Stash gating | "Oniki/Settings.cs" | "ShouldStashTravelDataOnWorldStop()" -> stash only if "GameStates.IsTravelling" **and** ("HasTravelData" **or** "sMovingWorldData != null"). Otherwise "ClearTravelData()". |
| Stale discard | "Oniki/Settings.cs" | "DiscardStaleTravelDataIfNeeded(isColdSettingsLoad)" -> on cold start, keep stash only when "ShouldApplyTravelDataOnColdStart()"; else clear and optional log. |
| Menu clear | "Oniki/KinkyMod.cs" | "KinkyMod.OnPreLoad" -> if "PersistStatic.MainMenuLoading", "Settings.ClearTravelData()". |
| World quit clear | "Oniki/KinkyMod.cs" | "OnLeaveWorld" / shutdown path -> "Settings.ClearTravelDataUnlessActiveWorldTransition()" (clear when main-menu loading or not travelling). |
| Cold start | "Oniki/Main.cs" | "Main.Start" -> call "DiscardStaleTravelDataIfNeeded(flag)" before travel restore branch; consume "TravelData" only on valid cold path, then "ClearTravelData()". |
| World stop | "Oniki/Main.cs" | "Main.Stop" -> "StashTravelDataOnWorldStop(__settings)" (replaces unconditional assignment). |

### Relation to "Outer's ReFiner" (separate mod)

- ReFiner **ReHelper** uses "[PersistableStatic]" toggles and a separate travel baseline (Build 449 wave); see ReFiner changelog. KW fix is **"Settings" object stash** ("TravelData"), not ReHelper persistable fields.

---

## Save load from main menu: "sPreLoaded" gate (hang / skipped preload)

### Background (player-visible symptom)

- In the **same game process**, after **Oasis Landing** (or any first load that completed KW preload), loading **another save from the main menu** (e.g. Isla Paradiso) could hang on the **first frame of the green loading bar** -> the phase where "KinkyMod.OnPreLoad" usually runs.
- The save file on disk was not necessarily corrupted; a **full game restart** often allowed loading again.
- Symptom aligned with **Oasis -> save -> main menu -> different save** in one session (same class of multi-save RAM issues as "TravelData", but a separate mechanism).

### Root cause (technical)

- "KinkyMod.OnPreLoad" used static **"sPreLoaded"**: after the first successful preload in a session, later "ObjectGroupsPreLoad" calls returned **immediately** without running enum patchers, "GuidMap", "IPreLoad" handlers, etc.
- In **445 v088** source, "sPreLoaded = true" was set **before** the heavy preload body; an interrupted preload could leave "sPreLoaded" true with incomplete init.
- "sPreLoaded" was **not** reset on "PersistStatic.MainMenuLoading" (unlike the intended pattern in older reference builds that reset preload state when appropriate).

### Changes ("Oniki/KinkyMod.cs")

| Behavior | Detail |
|----------|--------|
| Menu load reset | At start of "OnPreLoad", if "PersistStatic.MainMenuLoading": "Settings.ClearTravelData()" and **"sPreLoaded = false"** so menu-originated loads always run full preload. |
| Success-only flag | **"sPreLoaded = true"** only after **"PreLoad: DONE"** (end of successful try block), not at the beginning. |
| In-world travel | Loads that are not "MainMenuLoading" keep the once-per-session preload optimization when "sPreLoaded" is already true (EA travel within the same session). |

---

## WooHoo "ChangeStage": null guard on period protection lookup

### Background (player-visible symptom)

- During an active **WooHoo loop**, a **ScriptError** ("System.NullReferenceException") could fire on **Mariah Yee** (or any female **Master** in vaginal stage) when the loop attempted **"ChangeStage"** -> e.g. natural stage progression, **next-stage hotkey** ("RequestNextStage" / **Q**), or **change-position hotkey** (**E**).
- The interaction could abort mid-sequence; unrelated to save corruption.

### Root cause (technical)

- "WooHooInstance.ChangeStage" / "ChangeStageInternal" call "SimData.Get(Master.SimDescription).IsPeriodProtected" when the **current** stage category is **Vaginal** and **Master** is female (tampon/pad/cup removal path).
- "SimData.IsPeriodProtected" forwarded to "OutfitTools.IsUsingPeriodProtection(mSim.CreatedSim)" with **no null check** on "CreatedSim".
- In rare timing windows during live WooHoo, "SimDescription.CreatedSim" could be **null** while "Master" (live "Sim") was still valid -> NRE inside "IsUsingPeriodProtection(Sim)".

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| Utility guard | "Oniki.Utilities/OutfitTools.cs" | "IsUsingPeriodProtection" / "IsUsingDirtyPeriodProtection" return **"false"** when "Sim" argument is **null**. |
| SimData guard | "Oniki.Gameplay/SimData.cs" | "IsPeriodProtected" / "IsPeriodProtectedDirty" return **"false"** when "mSim.CreatedSim" is **null** before calling OutfitTools. |

### Design note

- **"ChangeStage" was not changed** to use "Master" instead of "simMaster.IsPeriodProtected": the guards above prevent the crash for all callers; swapping to "Master" would only matter for a separate semantic edge case (protection buffs on live Sim while "CreatedSim" is temporarily null).
- **Hotkey mapping** (E/Q vs CTRL+E/Q) is unchanged in this fix; optional follow-up if accidental keypresses remain annoying.

---

## WooHoo loop tick: defensive null guards ("WooHooInstance.Loop")

### Background (player-visible symptom)

- During an active **WooHoo loop** ("WooHooLoop" master tick), a **ScriptError** ("System.NullReferenceException" in "WooHooInstance.Loop") could abort the session for the master Sim (reported e.g. on **Salvador Albert** in Build 449).
- Unrelated to spanking social traces or "SocializeProxy" warnings in the same quit export; same class of crash as mid-sequence invalid participant/master state.

### Root cause (technical)

- "Loop()" validates once via "IsValid()", then dereferences **Master**, **Stage** / "mStage", **SimEntry** / live **Sim**, **BuffManager**, and **SimData** without re-checking when EA queue/state changes mid-tick (partner destroyed, interaction swapped, missing "SimData", SMC first-enter on stale master ref).
- "UpdateMotives()" used "SimData.Get(...)" without a null guard (same failure class as "Sink_WashHands" in Build 449).
- "ChangeStageInternal()" (and duplicate stage paths) used "simMaster.IsWearingPad" / "IsPeriodProtected" without checking "SimData.Get(Master.SimDescription)" for null.

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| SMC enter | "Oniki.Gameplay/WooHooInstance.cs" | Guard "Master" / "SimDescription" / "mSims" / "SimEntry" before first SMC actor-name setup. |
| Stage chance | "Oniki.Gameplay/WooHooInstance.cs" | Resolve "Stage" with null/empty-key guard; skip "WooHooStagesChance" write if "Main.Settings" null. |
| Participant tick | "Oniki.Gameplay/WooHooInstance.cs" | Skip null/destroyed sims and null "BuffManager" in motive/buff foreach; guard "mSims" null. |
| Sync increment | "Oniki.Gameplay/WooHooInstance.cs" | Guard "Master" before "SynchronizationLevel++"; guard live "Sim" on restart reposition loop. |
| Motives | "Oniki.Gameplay/WooHooInstance.cs" | "UpdateMotives": return early when "SimData.Get" is null. |
| Stage change | "Oniki.Gameplay/WooHooInstance.cs" | All "simMaster.IsWearingPad" / "IsPeriodProtected" branches require "simMaster != null". |

### Design note

- On guard failure, "Loop()" returns **"false"** so "WooHooLoop" ends the session cleanly instead of throwing; no gameplay progression is forced from a corrupt instance state.

---

## WooHoo SMC: yield guard on "RequestState" / "EnterState" ("Attempting to yield in a non-yielding context")

### Background (player-visible symptom)

- During an active **WooHoo loop** ("WooHooLoop.Run" -> "WooHooInstance.Loop"), a **ScriptError** could report **"System.NotSupportedException: Attempting to yield in a non-yielding context!"** inside **"StateMachineClient.RequestState"** (stack: "WooHooInstance.RequestState("Loop")" -> "WaitNextEvent" / "sim_sleep").
- Distinct from the **"NullReferenceException"** guard wave above: here the crash class is **illegal SMC yield**, not a null dereference.
- Session could degrade (animation stall / early abort); exception was often **caught and logged** via "Log.Write(ex)" rather than hard-crashing the game process.

### Root cause (technical)

- Each master **"Loop()"** tick calls **"RequestState("Loop")"** on the shared WooHoo SMC ("woohooinstance") to advance animation states for all scene actors.
- "Loop()" only checked **"Simulator.CheckYieldingContext(false)"** at entry. **"mSMC.RequestState"** is stricter: it internally yields ("WaitNextEvent") and requires the master’s **"Simulator.CurrentTask"** to still match **"Master.Proxy.ObjectId"** and the SMC binding to remain valid.
- Under **push/cancel**, **partner desync**, **stage restart** ("DestroySMC" / "CreateSMC"), or **nested interaction churn**, yielding context could pass the loose check while **"RequestState"** was no longer safe -> repeated **"NotSupportedException"** logged to ScriptError.
- Same risk on **"EnterState("Enter")"** and **"DestroySMC"** exit path ("RequestState(..., "Exit")").

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| Shared guard | "Oniki.Gameplay/WooHooInstance.cs" | **"IsSmcYieldContextValid()"**: "Master" valid + "CheckYieldingContext(false)" + "CurrentTask == Master.Proxy.ObjectId" (aligned with existing **"Pause()"** / **"Stop()"** checks). |
| Diagnostic | "Oniki.Gameplay/WooHooInstance.cs" | **"LogSmcYieldGuardSkip(op, state)"** -> **"WriteGeneral"** with prefix **"[WOOHOO-SMC-GUARD]"** when **Miscellaneous Logging** + global buffer ON (includes "yielding", "currentTask", "masterTask"). |
| Loop tick | "Oniki.Gameplay/WooHooInstance.cs" | After yielding check, **"Loop()"** returns **"false"** if SMC context invalid (skip tick instead of calling **"RequestState"**). |
| SMC API | "Oniki.Gameplay/WooHooInstance.cs" | **"EnterState"** / **"RequestState"**: early return when guard fails; existing per-call **"try/catch"** retained as safety net. |
| Teardown | "Oniki.Gameplay/WooHooInstance.cs" | **"DestroySMC"**: skip **"Exit"** **"RequestState"** when guard fails; still **"RemoveAllEventHandlers"** / **"Dispose"**. |
| Non-sync path | "Oniki.Gameplay/WooHooInstanceNonSync.cs" | Same guard on overridden **"EnterState"** / **"RequestState"** / **"DestroySMC"** (per-participant SMC). |

### Design note

- Prefer **skip + clean loop exit** over catching yield exceptions every tick; avoids ScriptError spam and matches **"Pause()"** / **"Stop()"** task-ownership semantics.
- When guard fires, WooHoo may end on the next **"Loop()" -> false"** from **"WooHooLoop"** -> acceptable vs. leaving a wedged SMC.

---

## Miscellaneous logging channel + spanking forensic trace relocation

### Background (player-visible symptom)

- Build 449 spanking wave forensic traces ("[SPANK_TEST]", "[ASK_TO_BE_SPANKED]", etc.) were written via **"Log.Write"**, which buffers into **"KWErrorLog_"**. Users enabling error-log export saw long spank/autonomy block spam mixed with real warnings/exceptions.
- No dedicated toggle/file for ad-hoc diagnostic strings that are not errors, interaction traces, or autonomy dumps.

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| New setting | "Oniki/Settings.cs" | **"MiscellaneousLogging"** (default **"false"**); migration in "ResetSettings", "Upgrade()", "EnsureReleaseRequiredSettingsPresent". |
| UI | "Oniki.UI/OptionSettingMenuMisc.cs" | Last item under **Misc -> Logging Features**. |
| Log API | "Oniki/Log.cs" | "ShouldLogMiscellaneous()" (requires **"EnableGlobalBuffer"** + toggle), "WriteGeneral()", "DumpGeneral()" -> export prefix **"General_Logging_"**, "ClearGeneral()". |
| Quit dump | "Oniki/KinkyMod.cs" | On world quit, dump/clear general log when toggle + global buffer active. |
| Spank Test | "Oniki.Gameplay.Punishments/Spank.cs" | "[SPANK_TEST]" ("Test()") and "[SPANK_RUN]" ("Run()") -> "WriteGeneral". |
| Ask flows | "Oniki.Interactions/AskToBeSpanked.cs", "AskToSpank.cs" | "[ASK_TO_BE_SPANKED]" / "[ASK_TO_SPANK]" push lifecycle -> "WriteGeneral". |
| WooHoo SMC guard | "Oniki.Gameplay/WooHooInstance.cs" | **"[WOOHOO-SMC-GUARD]"** skip diagnostics via **"LogSmcYieldGuardSkip"** (see SMC yield-guard section). |

### STBL setting entry (required for menu label)

- "Oniki.KinkyMod.OptionSettings.MiscellaneousLogging"
- *(hash TBD in package pass)*
- "EN: Miscellaneous logging"

---

## Logging channel separation (Interaction vs General)

### Background

- **"DebugInteractionLogging"** / **"KWInteractionLog_*.xml"** had accumulated non-notification diagnostics (outfit/CAS, pie menu, STC, WooHoo cleanup, CAS futa, violence witness, masochist rough bliss duplicate) via shared **"WriteInteraction"** buffer.
- **"MasochistRoughBlissTools"** wrote the same lines to **"WriteInteraction"** and **"Log.Write"** (dual export: interaction log + **"KWErrorLog_"**).

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| Interaction API | "Oniki/Log.cs" | **"WriteInteraction"** gated by **"ShouldLogInteraction()"** ("DebugInteractionLogging" + global buffer). |
| General API | "Oniki/Log.cs" | **"WriteGeneral"** gated by global buffer only; each caller keeps its own toggle gate. **"ShouldDumpGeneralLog()"** dumps when buffer non-empty or any of **Miscellaneous / SimOutfit / PieMenu / STC** toggles ON. **"WriteCasFutanariBodyDiag"** -> general buffer. |
| Quit dump | "Oniki/KinkyMod.cs" | **"KWInteractionLog_"**: **"DebugInteractionLogging"** only. **"General_Logging_"**: **"ShouldDumpGeneralLog()"** (OR of forensic toggles + non-empty buffer). |
| Outfit / CAS | "OutfitManager.cs", "OutfitTools.cs", "WooHooInstance.cs" | "[KW-UNDERWEAR-DEBUG]", "[KW-WOOHOO-OUTFIT-CLEANUP]", "[KW-FUTA-ERECTION-LAYERS]" -> **"WriteGeneral"** (still **"SimOutfitLogging"** gate). |
| Pie menu | "SocializeProxy.cs", "WooHooTools.cs", "ActionDataTunings.cs" | "[KW-PIEMENU-*]" -> **"WriteGeneral"** (still **"PieMenuLogging"** gate). |
| STC | "Oniki.UI/Hud.cs" | "[KW-STC]" -> **"WriteGeneral"** (still **"STCLogging"** gate). |
| Violence witness | "ViolenceSocialTools.cs" | "[ViolenceWitness]" -> **"WriteGeneral"** (still **"DebugInteractionLogging"** gate; no longer in interaction file). |
| Masochist rough bliss | "MasochistRoughBlissTools.cs" | **"[MasochistRoughBliss]"** -> **"WriteGeneral"** only; removed duplicate **"Log.Write"**; gate **"MiscellaneousLogging"**. |
| Notification trace | "Oniki/UITools.cs" | **"[KW-INTERACTION-DEBUG]"** remains sole **"WriteInteraction"** consumer -> **"KWInteractionLog_"**. |

---

## Human hygiene autonomy: missing need pipeline (standard + Enhanced Basic)

### Background (player-visible symptom)

- Human Sims could sit at **red hygiene (~-100)** for long stretches while **bladder / hunger / energy** were still handled normally by KW autonomy.
- **Mermaid hydration** (shower / bath / sponge) worked as expected; the gap was specific to **"CommodityKind.Hygiene"** on non-mermaid Sims.
- Not a random runtime bug: hygiene was **never wired** into the same need-detection / suggestion paths as the other base motives.

### Root cause (technical)

- **Enhanced Basic Autonomy** ("AutonomyManager"): "TryGetNeedMoodletCommodity", "BuildNeedMoodletSignature", "HasCriticalNeedMoodlet", need-while-busy preemption, and token resolution covered bladder, hunger, energy, vampire thirst, and mermaid hydration -> **not hygiene**.
- **Standard KW autonomy** ("KWAutonomy.ApplyMoodletNeedOverrides"): moodlet branches for bladder, energy, hunger, vampire thirst, mermaid hydration -> **no "CommodityKind.Hygiene" branch**; "HasNeedMoodletForCommodity(Hygiene)" always returned false.
- Shower / bath / sink tokens in "IsNeedLikeToken" were mapped only to **"MermaidDermalHydration"**, so human hygiene recovery actions were not classified as hygiene-need interactions in the busy-override layer.

### Design intent after fix

- **Human-only** ("HasMotive(Hygiene)" and **no** "MermaidDermalHydration" motive): sirena path unchanged.
- **Dedicated thresholds** (less aggressive than bladder/fame/energy defaults):
  - **Suggestion / warning:** motive ≤ **-60** and/or EA buff **"Smelly"** / **"Grungy"**
  - **Critical:** motive ≤ **-80**
- **Standard autonomy:** "ApplyMoodletNeedOverrides" runs **"FindBestInteraction(Hygiene)"** when the signal fires (both **"KWRunAutonomy" ON and OFF** paths).
- **Enhanced Basic:** same commodity in scan, signature, critical exempt-from-queue-budget, while-busy dispatch, preempt owner, and “do not interrupt hygiene recovery in progress” guard.

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| Thresholds + human gate | "Oniki.Gameplay/AutonomyManager.cs" | "kHumanHygieneWarningThreshold" (-60), "kHumanHygieneCriticalThreshold" (-80), "HasHumanHygieneMotive", per-commodity threshold helpers. |
| Enhanced Basic scan | "Oniki.Gameplay/AutonomyManager.cs" | Hygiene in "TryGetNeedMoodletCommodity", "BuildNeedMoodletSignature" ("Y" segment), "HasCriticalNeedMoodlet", "TryGetCriticalNeedCommodityExcludingEnergy", trackable/unsatisfied owner logic. |
| Token / recovery | "Oniki.Gameplay/AutonomyManager.cs" | "IsNeedLikeToken(Hygiene)" for shower/bath/sink/washhands/sponge/hygiene; actor-aware "TryResolveNeedCommodityFromToken"; "IsHygieneRecoveryInteraction" + defer-while-recovering guard. |
| Standard need branch | "Oniki.Gameplay/KWAutonomy.cs" | "HasHumanHygieneNeedSignal", "HasNeedMoodletForCommodity(Hygiene)", "ApplyMoodletNeedOverrides" hygiene branch; trace commodity list includes Hygiene. |

### Priority note

- Warning-stage **pick order** remains: bladder -> hunger -> energy -> vampire thirst -> mermaid hydration -> **hygiene last** among configured base needs (hygiene is less immediately life-threatening than bladder/hunger/energy).

---

## Social sync: WooHoo ask commit guard ("WooHooFlowGuard")

### Background (player-visible symptom)

- During a **player WooHoo ask** ("WooHooProxy" / "WooHooSequence" on the asking selectable Sim), if another Sim queued a **KW social toward the asker** on the same tick (common after **pause + dual queue**, or autonomous **Tease / Seduce** toward the asker at resume), the ask could **vanish from queue** and the asker became the **passive** partner of the interrupting social -> WooHoo never started.
- Repro: pause -> A asks B -> queue B->A Tease while paused -> unpause -> stage picker on A -> after picker, ask gone if B's social committed toward A.

### Root cause (technical)

- "KWSocialInteraction.RunInternal" always pushed "KWSocialInteractionPassive" onto the **target** queue with no check for an in-flight WooHoo ask on that target.
- Same-tick resume after pause made the race deterministic.

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| Commit window | "Oniki.Utilities/WooHooFlowGuard.cs" | **"WouldBlockSocialSync"**: block passive sync when target has **"WooHooProxy"** or **"WooHooSequence"** current/queued toward the social actor (ask phase only; not post-accept routing). |
| Guard hook | "Oniki.Interactions/KWSocialInteraction.cs" | Before passive "Add", skip sync and **"return false"** when guard fires; ask queue preserved. |
| Diagnostics | "Oniki/Log.cs", guard call site | **"[WOOHOO-SMC-GUARD][BLOCK_SYNC]"** via **"WriteWoohooCommitGuard"** when **Miscellaneous Logging** + global buffer ON and target is selectable. |

### Scope note (EA / vanilla socials)

- Guard applies to **KW socials** routed through **"KWSocialInteraction.RunInternal"** (Tease, Seduce, DareTo, etc.).
- **Vanilla EA friendly socials** (e.g. “Chat about day”) can still acquire sync toward the asker during the commit window -> unchanged in v1; only KW passive sync is blocked.

---

## "KWSocialInteraction": null guard on passive "CreateInstance"

### Background (player-visible symptom)

- **"NullReferenceException"** in **"KWSocialInteraction.RunInternal"** (reported e.g. **"DareTo"** Sandra Yee -> Ben Yee) when passive instance creation failed and code dereferenced a null passive.

### Changes ("Oniki.Interactions/KWSocialInteraction.cs")

- After **"KWSocialInteractionPassive" "CreateInstance"**, if cast is **null**: reset **"mStarted"**, debug notify, **"return false"** -> no NRE on **"LinkedInteractionInstance"**.

---

## Player input bypass: remove redundant "IsActiveSim" gate

### Background (player-visible symptom)

- With **Selectable Sims always accept player input** ON and **Sex gameplay -> High autonomy** (player input off), a **player-directed** KW social on selectable household Sim **B** could still run **NPC rejection** ("Interaction:GenericRejected") if the plumbob moved to another selectable (**A**) before the queued action **committed** -> even though the action was queued as manual ("Autonomous=false").

### Root cause (technical)

- Several **"Accepted()"** paths required **"Actor.IsSelectable && Actor.IsActiveSim"** for the bypass.
- **"!Autonomous"** already distinguishes player-directed vs autonomous; **"IsActiveSim"** only reflected plumbob at commit time, not player intent.
- **"WooHooSequence"** already used **"Actor.IsSelectable || Target.IsSelectable"** without **"IsActiveSim"**.

### Changes (files)

| Interaction module | File |
|--------------------|------|
| Tease, Seduce, Grope | "Tease.cs", "Seduce.cs", "Grope.cs" |
| Exhibition show / flash | "ShowBoobs.cs", "ShowBra.cs", "ShowPanties.cs", "ShowBottom.cs", "ShowFeet.cs", "Flash.cs", "FlashVoyeur.cs", "TeaseVoyeur.cs" |
| Undress | "UndressTop.cs", "UndressBottom.cs" |

- Bypass condition is now: **"!Autonomous && SelectableSimsAlwaysAcceptPlayerInputs && Actor.IsSelectable"** ( **"IsActiveSim" removed** ).

### Design note

- **Autonomous** NPC socials unchanged ("Autonomous=true" skips High-autonomy player-input rejection branch).
- **Non-selectable** actors still use NPC scoring when player-input bypass does not apply.
- **"SelectableSimsAlwaysPromptOnIncomingWooHoo"** / force-prompt logic still uses **"IsActiveSim"** where intentional -> not part of this pass.

---

## QoL: "Ask to be invited over" (in-world Friendly social)

### Background (player request)

- Vanilla exposes **Invite over** (bring the target to **your** home) via phone / face-to-face socials and random NPC->player invites, but **no** player-facing in-world action to ask a **present Sim** to invite **you** to **their** home.
- Build 450 adds a **manual-only** Friendly bucket interaction on **clickable Sims in the world** (not phone, not relationship panel, no KW settings toggle -> active whenever KW is enabled).

### Design (approved)

| Area | Decision |
|------|----------|
| Menu | **Friendly** ("CommodityTypes.Friendly" on "KWSocialInteraction" definition) |
| Injection | "InteractionInjector.Add<Sim>" + "InjectableInteraction" discovery (same pattern as "AskToSpank") -> **not** Kinky root menu |
| Autonomy | Hidden when "isAutonomous" |
| Acceptance | EA **Invite over** thresholds with **roles inverted** (host = target, guest = actor); post-check "RentScheduler" + "GroupingSituation" soft/hard reject on guest toward "target.LotHome" |
| Rejection UI | **"AskFor"** animation family ("social_askfor") -> awkward / neutral / reject STBL -> **no** EA phone "InviteOver:TestFailed" modal |

### Visibility ("Test()")

| Condition | UI |
|-----------|-----|
| Same household or same "LotHome" | **Hide** |
| Not socializable (EA invite rules, age, ghost, proprietor turn, "CanSocializeWith") | **Hide** |
| Autonomous | **Hide** |
| Actor already on "target.LotHome" | **Grey-out** + "TooltipAlreadyThere" |
| Target has no valid invite home (see below) | **Grey-out** + single "TooltipNoValidHome" |

**Valid invite home (v1):** "LotHome != null", not "WorldLot", "HomeWorld ==" current world; excludes base camp, dorm, apartment, commercial EA invite lots (tomb landmark/hidden, diving), homeless / non-residential lots (aligned with "CanInviteOverToLot" simplification).

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| New interaction | "Oniki.Interactions/AskToBeInvitedOver.cs" | "AskToBeInvitedOver : AskFor, InjectableInteraction"; "InviteOverAcceptanceTools" for inverted LTR/chance; "HandleAcceptedVisit" routing |
| Build | "Oniki_KinkyMod.csproj" | Compile include for "AskToBeInvitedOver.cs" |

**Accept flow (summary):**

1. "GetResult()" -> "InviteOverAcceptanceTools.TryEvaluateHostAcceptance(actor, target)"
2. "AskFor" animation by Accepted / Rejected / Awkward
3. On accept: TNS on **target** (host) + "PrepareSimsForInviteHomeTravel" (both Sims removed from "GroupingSituation") -> "SetInvitedOver" / "GreetSimOnLot" -> travel:
   - Host **already home** -> visitor "VisitLot" / "GoToLot" only
   - Host **away** -> host "GoHome" with visitor in **"SimFollowers"** (mirror "SocialCallback.OnInvitedOver" with inverted roles); fallback "PushAsContinuation" if needed
4. Events: "kInvitedSimOver(host, visitor)" + "kWasInvitedOver(visitor, host.SimDescription)"

### Post-prototype fixes (routing / EA popup)

| Issue | Fix |
|-------|-----|
| After accept, **visitor** traveled but **host** stayed on group outing lot | When "host.LotCurrent != host.LotHome", push **"GoHome"** on host with visitor in **"SimFollowers"** (EA "GoHome" only auto-clears grouping for NPC actors) |
| EA phone-style **"unexpected event"** dialog on valid accept | Unconditional **"GroupingSituation.RemoveSimFromGroupSituation"** on host + visitor **before** travel; removed "Phone.CallInviteOver.ShowInviteFailedDialog" fallback |

### STBL entries (STBL)

Runtime key = "Oniki.KinkyMod.<LogicalKey>" via "UITools.Localize". Hashes to be added in STBL editor.

| Full key | Hash | EN |
|----------|------|-----|
| "Oniki.KinkyMod.AskToBeInvitedOver:InteractionName" | *(TBD)* | Ask to be invited over |
| "Oniki.KinkyMod.AskToBeInvitedOver:TooltipNoValidHome" | *(TBD)* | *(single tooltip for all invalid-home cases)* |
| "Oniki.KinkyMod.AskToBeInvitedOver:TooltipAlreadyThere" | *(TBD)* | You're already at {0}'s home! |
| "Oniki.KinkyMod.AskToBeInvitedOver:Reject" | *(TBD)* | *(in-world reject line)* |
| "Oniki.KinkyMod.AskToBeInvitedOver:RejectBadly" | *(TBD)* | *(reject with negative STC)* |
| "Oniki.KinkyMod.AskToBeInvitedOver:AcceptTns" | *(TBD)* | Yeah, sure! Let's go to my place. |


### Out of scope (v1)

- Relationship panel / phone
- Cross-world travel to a homeworld that is not the current world
- NPC autonomy for this interaction
- Dorm / apartment as invitable homes
- Dedicated KW settings toggle

---

## Bug fix: orphaned "FollowSim" after "AskToWooHooWith" reject / abort

### Background (player-visible symptom)

- Reported on **Build 449** (including **teen** Sims with Teens ON): after a **multi-step sex request** ("Ask to WooHoo With"), the **initiator (A)** could keep a stuck **"Follow…"** ("FollowSim") action in queue even when the actual WooHoo was **refused** by the third Sim (**C**).
- First occurrence: severe lag / perceived freeze; **Reset Sim** on the initiator often cleared it. Second occurrence: **full session freeze**.
- User report did not always specify **direct 1-to-1 WooHoo** vs **"AskToWooHooWith"**; this fix targets the **latter** path only ("FollowSim" is **not** queued on direct "WooHooSequence" / "AskForWooHoo" 1-a-1 by design).

### Flow (intended "AskToWooHooWith" A -> B -> C)

| Step | Sim | Action |
|------|-----|--------|
| 1 | **A** | Asks **B** to have sex with **C** ("AskToWooHooWith") |
| 2 | **B** | Accepts or rejects the **meta-request** |
| 3 (meta accepted) | **B** | Runs "WooHooSequence" as **Actor** asking **C** ("Target") |
| 3 (meta accepted) | **A** | **"FollowSim"** queued (~**60 sim minutes**) to follow **B** |
| 4 | **C** | Accepts or **rejects** the real WooHoo |

On meta accept, "AskToWooHooWith.PostSocialLoop" pushes "WooHooSequence" on **B** and "FollowSim" on **A** ("FollowSim" STBL: "Oniki.KinkyMod.FollowSim:InteractionName"). "FollowSim" is also used elsewhere by design (**Escort**, **PhotoShoot**, **MovieShoot**) -> unchanged in this pass.

### Root cause (technical)

- "FollowSim" was pushed on **A** only after **B** accepted the meta-request, but **no cleanup** ran on **A** when **B**'s child "WooHooSequence" ended in **reject**, **cancel**, **setup failure**, or mid-flow threesome/whore reject.
- "FollowSim.Run()" loops for up to **60 sim minutes** with routing/idle work -> perceived **freeze** / lag until queue clear or Sim reset.
- Behavior present in **legacy Build 442** sources -> not a Build 449 regression; 449 exposed it via user report.

### Fix (strategy)

1. **"AskToWooHooWith"**: on every accepted meta path, set "wooHooSequence.AskToWooHooWithFollower = Actor" before pushing the child sequence.
2. **"WooHooSequence"**: release follower when WooHoo **does not** proceed to "WooHooGoTo"; keep follow when it **does** (legacy watch/cuckold behavior).
3. **"mAskToWooHooWithWooHooProceeding"**: set **true** immediately before "WooHooGoTo" push on accept -> success path **retains** "FollowSim" on **A**.

### Changes (files)

| Area | File | Behavior |
|------|------|----------|
| Cancel utility | "Oniki.Utilities/SimTools.cs" | **"CancelFollowSim(follower, followTarget)"** -> cancel active/queued "FollowSim" toward that target |
| Meta handoff | "Oniki.Interactions/AskToWooHooWith.cs" | **"AskToWooHooWithFollower = Actor"** on accepted meta before child push |
| Child cleanup | "Oniki.Interactions/WooHooSequence.cs" | Field **"AskToWooHooWithFollower"**; **"ReleaseAskToWooHooWithFollowerIfNeeded()"**; hooks in **"Run()"** (try/finally + early setup fail), **"Cleanup()"**; proceed flag on **"WooHooGoTo"** path |

### Follow cleanup matrix (post-fix)

| Outcome on B -> C "WooHooSequence" | "FollowSim" on A |
|-----------------------------------|------------------|
| Dialog/scoring reject | **Cancelled** |
| Player cancel | **Cancelled** |
| "Accepted()" false / setup failed | **Cancelled** |
| Threesome / whore reject mid-"PostSocialLoop" | **Cancelled** |
| Accept -> **"WooHooGoTo"** pushed | **Kept** (legacy) |

### Scope note

| Scenario | Covered by this fix? |
|----------|----------------------|
| **"AskToWooHooWith"**, C rejects at step 4 | **Yes** |
| **Direct** WooHoo ask A -> B, reject | **No** -> no "FollowSim" in that path; investigate separately if "Follow…" still appears |
| Escort / photo / movie crew follow | **No** -> intentional, untouched |

### STBL / settings

- **No new STBL** (no new player-facing strings).
- Teen path: unchanged rules -> "AskToWooHooWith" blocks underage only when **Teens OFF**.

---

## Bug fix: kinky TV autonomy -> black screen, stuck Sim, dead cancel

### Background (player-visible symptom)

- With **autonomous** kinky TV watching: TV could show a **persistent black screen** with on/off state ambiguous; the Sim could start **other actions on top** (e.g. **work out on the TV**) and remain **stuck** with no clean end.
- Player **cancel (red X)** often **did nothing** until **Reset Sim**.
- Typical context: autonomy + kinky channels + interrupted channel change, WooHoo/masturbation handoff from couch, or TV/workout state machine conflict.

### Root cause (technical -> chained failure)

| Layer | Issue |
|-------|--------|
| **Orphan channel** | Kinky channels live in "TVChannelData.sTVChannelDictionary" ("AddKinkyChannels" / "RemoveKinkyChannels"). EA "TV.ChangePlayingChannel" does **not** call "PlayGivenChannel" when "CurrentChannel == null" but "mPlayingChannel" still names a removed key -> **on + black screen** (e.g. TV on "KinkyChannel*" after mod OFF or channel removal). No KW cleanup pre-fix. |
| **Zap deadlock** | "ChangeChannel()" -> "mZappingChannel" + EA one-shot; if interrupted (WooHoo, masturbation, cancel, workout, SACS stuck), "mZappingChannel" / "bCalledChangeChannelCallback" can linger -> "CanChangeChannel()" blocks all watchers. |
| **Dirty watch exit** | Several "WatchTV" branches used "AddExitReason(ExitReason.StageComplete)" (sleep, masturbation, WooHoo, parental) **without** clearing pending zap -> desynced TV state machine and queue (matches useless cancel). |
| **"IsAppropriate" inverted** | Legacy used "(Main.Settings.Enabled \|\| underage \|\| WooHooLess) && Kinky" -> **false**, so with mod **ON** every kinky channel was “inappropriate” -> forced "SetAppropriateChannel(force: true)" on every "WatchTV" entry on kinky -> continuous re-zap churn even when the Sim **wanted** kinky TV. |
| **Workout overlap** | Workout and watch share TV state machine / "bIsBeingUsedForWorkOut"; corrupt TV state raised stuck workout/cancel risk. |
| **Minor guards** | "AddInteractions" indexer on orphan "mPlayingChannel", null "CurrentChannel" in EA skill check path, "WatchTVAutonomously" queuing watch with invalid dictionary channel. |

**"IsAppropriate" vs "WatchTV:ZapMiss" (unchanged by this fix):**

| Mechanism | Role |
|-----------|------|
| **"WatchTV:ZapMiss"** ("Oniki.KinkyMod.WatchTV:ZapMiss" -> EN: *"Uh oh, {0.SimFirstName} switched to the wrong channel."*) | "GetPreferedChannel" only: mod ON, Sim on **non-kinky** channel, **1/20** autonomous zap roll -> lands on kinky “by mistake” |
| **"IsAppropriate"** | Permission gate: kinky blocked when mod **OFF**, underage, WooHooLess, or KinkyChannel5 without ZooLover |

Post-fix: non-aroused adult on kinky is **not** forced to vanilla **on sit-down**; may stay until **"ConsidereZapping"** (High autonomy). Immediate re-zap on sit for non-aroused = **optional backlog** (do not reintroduce "Enabled \|\|" bug).

### Fix (five-point patch)

| # | Area | Behavior |
|---|------|----------|
| 1 | **Sanitize orphan channel** | "TVChannel.SanitizeTvPlayingChannel" -> if TV on and "mPlayingChannel" missing from dictionary -> "PickRandomChannel" or "TurnOff()"; "SanitizeAllTvsWithInvalidPlayingChannel()" on "RemoveKinkyChannels()"; hooks at "WatchTV.Run" start, TV-already-on branch, "WatchTVAutonomously.Run" before queue |
| 2 | **Zap timeout / cleanup** | "kZappingChannelTimeout" = **8 sim seconds**; "UpdatePendingChannelChangeState", "ClearPendingChannelChange", cleanup after "DoLoop"; "PrepareWatchExitForContinuation()" clears zap + "UseRemote = false" |
| 3 | **"IsAppropriate"** | "(!Main.Settings.Enabled \|\| underage \|\| WooHooLess) && Kinky" -> false -> mod ON + eligible adult -> kinky **allowed**, no spurious forced re-zap on entry |
| 4 | **Dictionary guards** | "TryGetValue" in "AddInteractions"; safe "ChangeChannel" / "UpdateChannel"; pregnancy effect hooks; "WatchTVAutonomously" **"return false"** if final channel not in dictionary |
| 5 | **Clean handoff exits** | "PrepareWatchExitForContinuation()" before "StageComplete" on couch sleep, kinky-TV masturbation, WooHoo join/inline, parental toddler exit |

### Changes (files)

| File | Modification |
|------|----------------|
| "Oniki.Objects.Electronics/TVChannel.cs" | "SanitizeTvPlayingChannel", "SanitizeAllTvsWithInvalidPlayingChannel"; hook on "RemoveKinkyChannels" |
| "Oniki.Interactions/WatchTV.cs" | "IsAppropriate" fix; sanitize; zap timeout/cleanup; dictionary guards; exit handoff helper |
| "Oniki.Interactions/WatchTVAutonomously.cs" | Sanitize target; validate channel; abort if invalid |

**Not modified:** "ChangeChannel.cs" (beyond prior patches), "_VID" resources in packages, TV XML tuning.

### Install note: "ONIKI_Videos.package" (separate from code fix)

- Core repo / "full_install" documented set has **no** kinky "_VID" assets; "TVChannel.cs" references **20** clip names ("KinkyChannel1_Vid_0" … "KinkyChannel5_Vid_3").
- **"ONIKI_Videos.package"** (forum addon, **not** in workspace mirror) supplies those **20 "_VID"** files -> **required** for visible kinky TV video; without it, "ObjectPlayVideo" on valid channels can still show **black screen** even when logic is correct.
- **"ONIKI_KinkyMod.package"** = channels, interactions, STBL channel names; **"ONIKI_Videos.package"** = video assets only. Vanilla channels unaffected.


### STBL / settings

- **No new STBL** for this patch.
- Existing **"Oniki.KinkyMod.WatchTV:ZapMiss"** flavor unchanged.

---

## High School: lecture performance, school profiler, nurse labels (Build 450 school wave)

### Background (player-visible symptom)

- With **High School** enabled and Sims in the **rendered** school lot during an active **school situation** (lecture / stage): **lag and micro-stutter every ~2–3 real seconds** at **Speed 1**, worse than many other KW contexts.
- Outside the lot or with KW off: often better (not 100% isolated in user tests).
- Separate issue (same wave): **University Life** pair social **Do School Cheer** missing from Friendly with KW ON -> see subsection below.

### Diagnosis (user "General_Logging_*.xml", pre-throttle)

| Scope | Role | Indicative metrics |
|-------|------|-------------------|
| **"assign.student"** | **Dominant** | ~85–96 samples; avg **~750–840 ms**; many **>500 ms** |
| **"lecture.update"** | 5 sim-min alarm batch | avg **~940–1380 ms**; peaks **~9–10 s** (all students assigned per tick) |
| **"assign.professor"** | Secondary | avg **~430–505 ms** |
| **"lot_scan.lunch_stage_init"** | One-shot spike | **~9 s** at lunch stage |
| **"simdata.update_broadcasters"** | **Not culprit** | ~**0.1 ms** per sample |
| **"naked_broadcaster.update"** | **Not culprit** | ~**0–1 ms** |
| **"woohoo_task.perform"** | **Absent** in exports | -> |"n
**Outlier Sims** (second log): e.g. Lil Bling / Jett Atkins -> repeated slow assigns (~1–10 s max); cost not uniform (outfit, routing, blocked queue).

**Hot path chain:**

"""text
LectureStage.UpdateLecture (alarm every 5 sim-min)
  -> AssignStudentBehaviour / AssignProfessorBehaviour
    -> (legacy) Tools.Sleep(20) + Assign*Internal
      -> FindRandomStudentLectureInteraction
        -> SwitchToOutfit (uniform) -> AssignClassroomToStudent -> ForceInteraction (Sit, TakeNotes, …)
"""

Post-B450-2 log ("69AFFE7C"): each assign still ~**700 ms**; **"force_interaction.normal"** often **0 ms** at old ≥16 ms threshold; **54** student assigns/session -> heavy **callback** churn, not alarm alone.

---

### Patch A -> School situation runtime profiler

| Field | Detail |
|-------|--------|
| Setting | **"SchoolSituationProfiling"** (default **"false"**) |
| UI | **Misc -> Logging Features** (after Miscellaneous logging) |
| Requires | **Enable Global Buffer** ON |
| Export | **"General_Logging_*.xml"** via "WriteGeneral" -> **not** "KWInteractionLog_*"; independent of Miscellaneous logging |
| Prefix | **"[KW-SCHOOL-PROFILE] scope=… elapsed_ms=…"** |
| Emit | **"elapsed_ms >= 16"** or scope heartbeat **≥ 2000 ms** real time |

**Migration:** "PreviousVersion < 450" add-if-missing "false"; "EnsureReleaseRequiredSettingsPresent"; "ResetSettings".

**Top-level scopes:** "lecture.update", "assign.student", "assign.professor", "woohoo_task.perform", "lot_scan.*", "simdata.update_broadcasters" (school lot only), "naked_broadcaster.update" (school lot + spectator count).

**STBL (package pass):**

- "Oniki.KinkyMod.OptionSettings.SchoolSituationProfiling"
- *(hash TBD)*
- "EN: School situation profiling"

**Files:** "SchoolSituationProfiler.cs" (new), "HighSchoolSituation.cs", "Settings.cs", "OptionSettingMenuMisc.cs", "Log.cs", "SimData.cs", "NakedBroadcaster.cs", "Oniki_KinkyMod.csproj".

---

### Patch B -> Lecture assign throttling (always on when patch deployed)

**"LectureStage.UpdateLecture":**

- Round-robin cursors; max **2** student + **1** professor assign per tick ("kMaxStudentAssignsPerLectureTick" / "kMaxProfessorAssignsPerLectureTick").
- **"ShouldSkipLectureStudentAssign"**: destroyed; "IsBusy"; "IsGoingToClassroom"; "IsLearning"; "!StudentCanChoseRandomInteraction".
- **"ShouldSkipLectureProfessorAssign"**: destroyed; "IsBusy"; "!ProfessorCanChoseRandomInteraction".
- **Failure retry** ("OnStudentFailure" / "OnProfessorFailure") still calls "Assign*Internal" directly (not budget-limited).

**Expected:** single-tick peak ~**7–10 s** -> ~**1.5–2 s** (2 student assigns vs full roster).

---

### Patch C -> Sub-scope profiler + Sim detail

**"BuildLectureAssignDetail(sim)"** on "assign.student" / "assign.professor":

"sim=… busy=… goingClass=… learning=… queue=… ix=… room=… classroom=… uniform=…"

**Sub-scopes:** "assign.student.outfit_switch", "assign.student.assign_classroom", "assign.student.force_interaction.prank|normal|sports|arts" (and professor equivalents where instrumented).

---

### Patch B450-2 -> Sleep removal, callback throttle, deep profiler

**Performance fixes:**

1. **Removed "Tools.Sleep(20)"** from lecture "AssignStudentBehaviour" / "AssignProfessorBehaviour" (and related headmaster assign path).
2. **Callback throttle** (lecture stage only): min **100 sim ticks** between callback-driven assign per Sim ("kMinTicksBetweenCallbackStudentAssign" / professor); "TryEnterLecture*Assign" logs "reason=callback_cooldown".
3. **Skip on callbacks:** "ShouldSkipLecture*" applied outside alarm loop (busy / learning / blocked queue).
4. **Alarm path:** "mLectureAssignFromAlarm" -> bypass callback cooldown; **2+1** budget unchanged.

**Deep profiler ("RecordPhaseDeep", emit ≥ **1 ms**):**

| Scope | Measures |
|-------|----------|
| "assign.student.start_working" | "StartWorking" |
| "assign.student.process_user_directed" | "ProcessUserDirectedInteraction" + "deferred=" |
| "assign.student.find_lecture" | Full "FindRandomStudentLectureInteraction" |
| "assign.student.outfit_switch" / "assign_classroom" | Previously invisible if &lt;16 ms |
| "assign.professor.start_working" / "find_lecture" | Professor path |
| "assign.*.skipped" | "busy_or_blocked" or "callback_cooldown" |
| Top-level assign | "source=alarm" / "source=callback" |

**Profiler wave B450-3** (residual freeze / auto-manage):

| Scope | When |
|-------|------|
| "school.auto_manage.tick" / "assign_students" | Profiler ON + **automatic school management** ON + school lot |
| "school.auto_manage.create_school" | "SimData.CreateSchool" (auto-manage ON) |
| "school.push_sims" / "school.push_sims.sim" / "school.push_sims.go_to_work" | School lot, 10 sim-min alarm |
| "school.stage.transition" | "SetStage" (Morning / Lunch / Club / …) |
| "school.indecent_behavior" / "try_join" / "punish" | WooHoo join / scold in class |
| "naked_broadcaster.pulse" | Spectator enters radius on school lot |

Helpers: "ShouldProfileAutoManage(lot)" vs "ShouldProfileSchoolLot(lot)".


---

### Fix -> High School nurse queue labels ("Nurse.cs")

**Symptom:** Teen patient queue showed raw **"BeExamined"**; nurse showed **"MedicalExamination"** (C# class names -> definitions lacked "GetInteractionName").

**Fix:** "GetInteractionName" -> "UITools.Localize":

| Class | Logical key | Hash | EN |
|-------|-------------|------|-----|
| "BeExamined" | "highschoolsituation/medicalexamination:beexamined" | "0x419F855A6E460A8A" | Be examined |
| "MedicalExamination" | "highschoolsituation/medicalexamination:interactionname" | "0x1B2B70A5F99D25E1" | Give medical examination |

Full keys: "Oniki.KinkyMod.highschoolsituation/medicalexamination:beexamined" / ":interactionname". Dialog STBL under same namespace ("medicalexamination:*") unchanged.

**IT (suggested):** *Essere visitato* / *Visita medica*.

---

### Fix -> University Life: "Do School Cheer" missing (Friendly pie menu)

**Symptom:** With KW **enabled**, **Friendly -> Do School Cheer** (UL, **Jock / Socialite**, both Sims influence ≥ 2) **vanished** on click Sim; solo **Perform School Cheer** still worked; disabling KW restored the option (reported Build 443+, confirmed Build 449).

**Cause:** "TestCanSchoolCheer" in "ActionDataTunings.cs" treated "SimData.OffCooldown("SchoolCheer", target) == true" (allowed) as **hide** when "SimData.Get(actor) != null" -> pair cheer **always** hidden under KW; no "AddCooldown("SchoolCheer")" in codebase.

**Fix:** Removed erroneous cooldown gate; **"TestSocial"** + EA **"SocialTest.CanSchoolCheer"** only. **"KWPracticeSchoolCheer"** (self) unchanged.

| Area | File |
|------|------|
| Procedural test | "Oniki.Tunings/ActionDataTunings.cs" |

---

### Files touched (school wave checklist)

| File | Change |
|------|--------|
| "Oniki.Utilities/SchoolSituationProfiler.cs" | New profiler API + "RecordPhaseDeep" |
| "Oniki.Situations/HighSchoolSituation.cs" | Throttle, skip, Sleep removal, callback gate, profiler hooks |
| "Oniki/Settings.cs" | "SchoolSituationProfiling" |
| "Oniki.UI/OptionSettingMenuMisc.cs" | Menu entry |
| "Oniki/Log.cs" | "ShouldDumpGeneralLog()" |
| "Oniki.Gameplay/SimData.cs" | School-lot broadcaster scope |
| "Oniki.Gameplay/NakedBroadcaster.cs" | School-lot update + pulse scope |
| "Oniki.Tunings/ActionDataTunings.cs" | Do School Cheer |
| "Oniki.Roles/Nurse.cs" | Interaction display names |
| "Oniki_KinkyMod.csproj" | "SchoolSituationProfiler.cs" compile |

---

## Performance wave: world runtime profiler + outfit/autonomy hot paths (Phases A–C1)

### Background (goal)

- Reduce **lag**, **micro-stutter**, and runtime buffer pressure by measuring **"elapsed_ms" per scope** before changing gameplay rules.
- Builds 444/445/450 addressed logging and **vertical** school hot paths; remaining cost is structural **per-Sim** work: **"SimData.Perform"**, outfit layer passes, autonomy **"FindBestInteraction"** on large queues.
- User correlation: disabling High School often removed home micro-stutter too -> consistent with **high autonomy queue** + school teen/NPC load, not only in-lot lecture code.

### Phase A -> World runtime profiler (implemented)

| Field | Detail |
|-------|--------|
| Setting | **"WorldRuntimeProfiling"** (default **"false"**) |
| UI | **Misc -> Logging Features** (after **School situation profiling**) |
| Requires | **Enable Global Buffer** ON + toggle ON (zero string overhead when OFF) |
| Export | **"General_Logging_*.xml"** -> prefix **"[KW-WORLD-PROFILE]"** |
| API | "Oniki.Utilities/WorldRuntimeProfiler.cs" -> "Run", "Record", "RecordPhaseDeep", "RecordSkip" |
| Session | "world.heartbeat" (15 sim-min), "world.session_summary" on quit (buffer sizes + profiler line counts) |

**Emit rules:**

- Outlier: **"elapsed_ms >= 16"** (same as school profiler)
- Heartbeat per scope: **≥ 2000 ms** real time if under threshold
- Deep sub-scopes ("find_best", outfit lock/layers, pulse): **≥ 1 ms**
- "*.skip" events: throttled ~**2000 ms** real time per scope (avoid N× idle flood)

**Instrumented scopes (summary):**

| Family | Examples |
|--------|----------|
| "simdata.perform" | Full tick; sub: "outfit", "outfit.skip", "start_naked_broadcaster", "womb", "update_broadcasters", "dominant_presence.sync" |
| "outfitmanager.*" / "outfittools.update_layers*" | "update", "wait_and_lock", layer/erection/cache passes, "*.skip" |
| "naked_broadcaster.*" / "sim_broadcaster.*" | "update", "pulse", "radius_scan" |
| "autonomy_manager.*" | "simulate_cycle", "find_best", "find_best.skip" |

**STBL (package pass):**

- "Oniki.KinkyMod.OptionSettings.WorldRuntimeProfiling"
- *(hash TBD)*
- "EN: World runtime profiling"

**Migration:** "PreviousVersion < 450" add-if-missing "false"; "EnsureReleaseRequiredSettingsPresent"; "ResetSettings".

**Phase A log findings (owner, pre-Phase B):**

| Excluded as primary | Evidence |
|---------------------|----------|
| "NakedBroadcaster" / "SimBroadcaster" | Zero lines in analyzed samples |
| "dominant_presence.sync" | 0–6 ms |

| Likely culprits | Evidence |
|-----------------|----------|
| "outfitmanager.update" / "simdata.perform.outfit" | Spikes **499 ms – ~22 s** (e.g. outlier Sim ~22 s) |
| "simdata.perform" baseline | **30–70 ms** × many in-world Sims |
| "autonomy_manager.find_best" | **200–441 ms** with queue **43–49** |

**Priority after data:** P0 outfit/layers; P1 Perform/autonomy tiering; P2 school off-lot idle; broadcaster/D/S low on these logs.

**Files:** "WorldRuntimeProfiler.cs" (new), "SimData.cs", "OutfitManager.cs", "OutfitTools.cs", "NakedBroadcaster.cs", "SimBroadcaster.cs", "AutonomyManager.cs", "Main.cs", "KinkyMod.cs", "Settings.cs", "OptionSettingMenuMisc.cs", "Log.cs", "Oniki_KinkyMod.csproj".

---

### Phase B -> Outfit gating + layer forensics (implemented)

**B1 -> Skip "OutfitManager.Update(true)" on idle/clean Perform ticks**

| API | Role |
|-----|------|
| "ShouldRunUpdateTick()" | True on post-load fixup, sync/dirty, swap/helper, underwear destroyed, CAS key mismatch, realtime erection delta, layer pass needed, or "ShouldRunFullUpdateTick()" |
| "ShouldRunFullUpdateTick()" | Post-load, blend pending, pubic hair interval, 12h outfit cache |
| "RequestSync()" | From "ChangeOutfit" / "SwitchToOutfit"; invalidates layer key |
| Perform hook | "Update(full)" only when "ShouldRunUpdateTick()"; else "RecordSkip("simdata.perform.outfit", reason=idle_clean)" |

**Event vs maintenance (by design):**

- **Events:** "ChangeOutfit", "SwitchToOutfit", "WooHooTools.MakeSimReady" -> direct undress; not dependent on Perform tick.
- **Maintenance:** Perform tick -> periodic layer/erection/dirty/pubic/cache sync.
- **No skip during:** active helper/swap, pending outfit, lock wait, visual buffs (creampie locations + "Beaten").

**B2 -> Anti-redundant "UpdateLayersAndErection" + deep profiler**

| API | Role |
|-----|------|
| "NeedsLayerAndErectionPass()" | Early exit when key match, stable erection, layers applied, no visual buffs |
| "MarkLayersAndErectionApplied()" / "RequestSync()" reset | Track last successful pass |
| "HasActiveVisualLayerBuffs()" | Blocks layer skip (creampie + beaten) |
| "mSyncRequested" clear | In "Update()" "finally" after successful lock; stays true if lock fails (retry next tick) |

---

### Phase C1 -> Autonomy queue budget + outfit lock/layer hardening (implemented)

**C1a -> Autonomy queue budget (P0)**

- Threshold: **"kAutonomyQueueBudgetThreshold = 30"**
- When queue ≥ 30: defer **"FindBestInteraction"** for NPCs that are **not** selectable, **not** active household, **without** critical need moodlet -> re-queue without scan
- **Protected:** selectable Sims, active household, critical needs, "TryProcessNeedWhileBusy" paths unchanged
- Profiler: "autonomy_manager.find_best.skip" + "reason=queue_budget queue=N"

**C1b -> Outfit lock fast-fail on Perform maintenance (P0)**

- "WaitAndLock(..., maintenanceFastFail=true)" **only** from Perform maintenance "Update()"
- If busy/waiting/pending outfit -> immediate "kInvalidLockId" + "RequestSync()" (retry next tick)
- **"SwitchToOutfit" / helpers / events** keep full wait path
- Profiler: "outfitmanager.update.skip reason=lock_unavailable maintenance=true"

**C1c -> Layer "SetOutfit" cooldown (P1)**

- **"mLayersSetOutfitCooldownTicks = 12"** Perform ticks after successful layer pass
- During cooldown: "NeedsLayerAndErectionPass()" false if key match + no dirty/sync/buff/realtime erection (>0.15)
- Reset on "RequestSync()" / "ChangeOutfit" / "SwitchToOutfit"
- Profiler: "outfittools.update_layers_and_erection.skip reason=layer_cooldown_or_stable"


---

### Not implemented (backlog from performance plan)

**Phase B2 quick wins (low priority on Phase A logs):**

- Throttle "StartNakedBroadcaster()" off every Perform tick
- NakedBroadcaster indoor: higher pulse interval, dynamic sleep without spectators
- D/S presence: same-room scan first

**Phase C (medium risk, post-C1 retest):**

- Tiered "SimData.Perform" (Tier 0/1/2)
- School teen/NPC idle work when player not on school lot
- Unified broadcaster scheduler + global scan budget
- Audit residual "Log.Write" without "IsGlobalBufferEnabled()" on hot paths

---

## Pie menu click profiling (Phase 0 only -> no gameplay change)

### Scope of this Build 450 entry (important)

**Shipped in production code:** diagnostic **instrumentation** and logging split only.

**Not shipped / reverted:**

- **Patch 1.0 -> SocializeProxy single-inject** was tried and **fully rolled back** after in-game **STC / romantic conversation regression** (e.g. parent->teen daughter: romantic social reset posture/STC). Production still uses **legacy dual inject** ("ProxyDefinition" + "ProxyDefinition_Incest" + "PostSweep") -> **unchanged pie menu behavior**.
- No reduction of injected interactions, no "PieMenu.sIncrementalButtonIndexing" toggle, no lazy injection, no pick-task KW shim.

**User-visible pie menu lag is not fixed by this wave** -> only measured and documented for future STC-safe work.

### Background (player-visible symptom)

- Many users perceive **delay between click and pie menu**, especially on **Sims** (even with few other mods).
- KW adds ~100+ "InteractionInjector" entries on "Sim", plus **"SocializeProxy"** (romantic/funny/incest buckets) -> each click runs EA collect -> validate -> pretest -> bring-up over a large candidate list.
- **World runtime / outfit / autonomy** optimizations (Performance Patch) do **not** cover click->menu systematically.

### What was implemented (Phase 0 -> logger & hooks)

| Component | Role |
|-----------|------|
| "PieMenuClickProfiler.cs" | Emit **"[KW-PIEMENU-PROFILE]"** + "GameStateFlags()" + "life_stage" on Sim targets |
| "PieMenuPickOutcome.cs" | Structured routing outcome ("Success" / "Defer") for instrumented vs fallback paths |
| "PieMenuPretestStats.cs" | Pretest pass/fail/removed counts |
| "PieMenuPickProfilerCore.cs" | Phases: **collect** / **validate** / **pretest** / **bring_up**; "IsPlayFlowState" aligned to EA "GameObject.OnPick" |
| "PieMenuPickProfilerTask.cs" | Wraps vanilla "PickObjectTask" when profiler ON; explicit fallback "reason=" ("route_no_guid", "build_buy", "cheat_menu", …) |
| "PieMenuPickProfilerBootstrap.cs" | Install ~**1 s** after world load (replaces assignable pick task e.g. NRaas Selector **only while profiling ON**) |
| "SocializeProxy.cs" | Scope **"piemenu.socialize_proxy.add"** (timing only) -> **injector registration unchanged** |
| "Main.Start" / "Main.Stop", "KinkyMod.cs" quit | Session summary "piemenu.session_summary" |

**Settings (logging only):**

| Key | Default | Menu | Output |
|-----|---------|------|--------|
| "PieMenuLogging" | "false" | Misc -> Logging Features | **"[KW-PIEMENU-PROFILE]"** timing (requires **Enable Global Buffer**) |
| "PieMenuDiagnosticLogging" | "false" | Under Pie menu logging | **"[KW-PIEMENU-*]"** forensic social (EA-FUNNY / ROMANCE, etc.) -> **not** timing |

Existing **"PieMenuLogging"** forensic paths in "SocializeProxy" / "WooHooTools" / "ActionDataTunings" remain on **"WriteGeneral"** when diagnostic toggle ON (unchanged behavior, clearer separation from profiler).

**Emit rules:** outlier **≥ 16 ms**; heartbeat **≥ 2000 ms** real time; deep sub-scopes **≥ 1 ms**; lines include "target_type", "interaction_count", pretest stats.

**Instrumented scopes:** "piemenu.click.total", ".collect", ".validate", ".pretest", ".bring_up", ".vanilla_fallback", "piemenu.socialize_proxy.add", "piemenu.pick_install.skip", "piemenu.session_summary".

**Files (new/extended):** "PieMenuClickProfiler.cs", "PieMenuPickOutcome.cs", "PieMenuPretestStats.cs", "PieMenuPickProfilerCore.cs", "PieMenuPickProfilerTask.cs", "PieMenuPickProfilerBootstrap.cs", "Oniki_KinkyMod.csproj", hooks in "Main.cs" / "KinkyMod.cs" / "SocializeProxy.cs" (timing hook only).

**No new STBL** for Phase 0 (reuses existing "PieMenuLogging" label; "PieMenuDiagnosticLogging" migration add-if-missing in "Settings").

---

### Profiler baseline (sample "General_Logging" export)

| Scenario | "click.total" | "collect" | "pretest" | "bring_up" | Notes |
|----------|---------------|-----------|-----------|------------|-------|
| Adult self (Oliver) | 342–425 ms | 268–364 ms | 38–59 ms | 22–86 ms | 869 collected -> 152 pretest pass |
| Teen household (Stella) | 346–408 ms | 293–362 ms | 19–31 ms | 15–74 ms | socialize ~121 ms (dual inject) |
| Strong LTR (Sandra) | 306–415 ms | 279–344 ms | 20–23 ms | 5–77 ms | "ProxyDefinition added=23" |
| Terrain | 18–34 ms | 1–3 ms | 9–17 ms | 8–14 ms | not primary target |
| Object (shower, bed, …) | 5–13 ms | 1–3 ms | 1–4 ms | 3–9 ms | no SocializeProxy |

**Conclusions:**

- **~75–85%** of Sim click lag = **"collect"** (not pretest).
- **Dual "socialize_proxy.add"** (~90–160 ms per Sim click, even "added=0" on self) = major contributor inside collect -> **still present in shipped build**.
- Zero **"vanilla_fallback"** in sampled play -> instrumented path used.
- Objects/terrain fast -> optimize Sim collect first.

---

### Attempted then reverted (not in release)

| ID | Change | Log | Result |
|----|--------|-----|--------|
| **1.0** | SocializeProxy **single-inject** (one "InteractionInjector" pass instead of two) | "010419CB" | ~45–90 ms saved on socialize line; **collect still ~300–430 ms**; **STC regression** on romantic in conversation -> **rollback** to dual inject |

**Phase 1 on hold** until STC-safe design or surface reduction (Phase 2 hub/lite menu).

**NRaas Selector (external):** with "PieMenuLogging" ON, bootstrap swaps in profiler task -> **no measurable win** on collect (~370–480 ms); Selector does not remove KW injection work.

---

## Keyboard shortcuts (master toggle + TRIG hooks)

### Overview

- **Player-facing master switch:** **Miscellaneous -> Enable keyboard shortcuts** (default **OFF**).
- When **ON**, DLL registers named **TRIG** map hooks from "ONIKI_KinkySettings.package" (and optional manual TRIG imports). **TRIG resources alone do nothing** without "KWTrigger.Start()" in the script assembly.

### Setting & persistence

| Field | Detail |
|-------|--------|
| Dictionary key | "EnableKeyboardShortcuts" |
| Property | "Settings.EnableKeyboardShortcuts" |
| Default | "false" ("ResetSettings", "EnsureReleaseRequiredSettingsPresent") |
| Migration | "PreviousVersion < 450" + missing key -> seed from tunable **"KinkyMod.kKeyboardShortcuts"** ("ONIKI_KinkySettings" "_XML"; absent -> "false") |
| Runtime source of truth | **Save dictionary** -> XML tunable is **read at mod load only** for migration, **not** rewritten in-game |

**Menu:** "OptionSettingEnableKeyboardShortcuts" -> **Miscellaneous**, second-to-last row (before **Interactions** submenu).

| Toggle action | Effect |
|---------------|--------|
| OFF -> ON | Save "true", "KWTrigger.ApplyRuntime(true, showNotification: true)" -> hooks + localized help popup |
| ON -> OFF | "KWTrigger.Stop()" -> all hooks removed |
| World load / "Settings.Apply()" | "ApplyRuntimeFromSettings()" -> **no** popup on load |

### TRIG package layout ("ONIKI_KinkySettings")

| Map instance name | Keys (when shortcuts ON) | Handler |
|-------------------|--------------------------|---------|
| "KWNextStage" | **Q** | WooHoo loop -> "RequestNextStage = true" (selected Sim) |
| "KWChangePosition" | **E** | WooHoo loop -> position picker / "ChangeStage" |
| "KWSaveGame" | **F5** | "OptionsModel.SaveGame(true)" |
| "KWOpenSettings" | **Ctrl+K** | "SettingsHotkeys" -> "OptionSettings.Show()" (same as pie **Settings**) |
| "KWDebugMove" | **Ctrl+** Enter / arrows / Page Up-Down | "DebugMoveHotkeys" -> **Debug -> Move** pie path ("MoveParameters") |

**Manual S3PE import:** TRIG instance names **"KWDebugMove"** and **"KWOpenSettings"** (shipped as separate trigger resources for "ONIKI_KinkySettings.package").

Legacy WooHoo/save TRIG (3 files) ship in package; Debug Move + Open Settings require **manual TRIG import** into "ONIKI_KinkySettings.package".

**TRIG authoring rules:** use **"control"** without **"?"** when Ctrl is required; **never** hook "script_ingame" on "SceneMgrWindow" (broke Delete/Esc in Buy Mode -> fixed ~449).

### Runtime ("KWTrigger.cs")

- "Start()" / "Stop()" -> "AddTriggerHook" / "RemoveTriggerHook" + "TriggerDown"
- **"OnTriggerDown" dispatch (Live only):** "SettingsHotkeys" -> "DebugMoveHotkeys" -> legacy WooHoo/save
- Lifecycle: "Settings.Apply()" end, "KinkyMod.OnShowVersion" (~1 s post load), "Main.Stop()" -> sync/stop

**Gates:**

| Feature | Requires |
|---------|----------|
| WooHoo E/Q, F5 | Shortcuts ON + Live + selected Sim in WooHoo loop (E/Q) |
| Ctrl+K settings | Shortcuts ON + Live (no Debug Mode) |
| Debug Move | Shortcuts ON + **Debug Mode** + Live + plumbob-selected Sim; set-value dialog also needs **"Settings.Enabled"** |

**Debug Move ≠ WooHoo pie Move** -> pie **Kinky World -> Move** uses "KWActorMoveValue" (LoversLab path); hotkeys mirror **Debug -> Move** only.

### Changes (files)

| File | Purpose |
|------|---------|
| "Oniki.Interactions/KWTrigger.cs" | Hook registry + dispatch |
| "Oniki.Interactions/SettingsHotkeys.cs" | Ctrl+K |
| "Oniki.Interactions.Debug/DebugMoveHotkeys.cs" | Ctrl+debug move |
| "Oniki.Interactions.Debug/MoveParameters.cs" | Shared move math + dialog ("OneShotFunctionTask" for modal) |
| "Oniki/KinkyMod.cs", "Oniki/Main.cs", "Oniki/Settings.cs" | Lifecycle + setting |
| "Oniki.UI/OptionSettingEnableKeyboardShortcuts.cs", "OptionSettingMenuMisc.cs" | Menu |

### STBL (STBL)

| Full key | Hash | EN |
|----------|------|-----|
| "Oniki.KinkyMod.OptionSettings.EnableKeyboardShortcuts" | *(TBD)* | Enable keyboard shortcuts |
| "Oniki.KinkyMod.Notification.KeyboardShortcuts.EnabledHelp" | *(TBD)* | *(multi-line help body when enabling shortcuts -> see section above for suggested EN list)* |

Suggested help body when enabling (EN):

"""
Kinky World keyboard shortcuts enabled:
Ctrl+K -> Open Kinky World settings
E -> Change position (during WooHoo)
Q -> Next stage (during WooHoo)
F5 -> Save game

Debug Move (Debug Mode required):
Ctrl+Enter -> Set move value
Ctrl+Arrow keys -> Move up / down / left / right
Ctrl+Page Up / Page Down -> Move forward / backward
"""

### Deploy checklist

1. Update **"ONIKI_KinkySettings.package"** TRIG resources (instance names must match table).
2. Replace **"Oniki_KinkyMod.dll"** (S3SA workflow) + clear **"scriptCache.package"**.
3. Add STBL for menu label + help notification.

---

## WooHoo packages: Scan for packages (auto-discovery)

### Overview

- **Menu:** WooHoo -> **Packages** -> **Scan for packages** ("OptionSettingScanPackages") -> single entry; experimental **Scan 2** / "DiskIndexOnly" **removed** from UI and code.
- **Goal:** Discover mounted WooHoo animation CC automatically, run the **same validation** as manual **Add package**, register new entries in "WooHooStagePackages", show confirm dialog -> progress -> **3-outcome result popup** -> reopen Packages menu.
### UX (Build 450)

| Step | Behavior |
|------|----------|
| Pre-scan | "AcceptCancelDialog" (same pattern as **Spread Kinky Traits**). **Cancel** -> "ResumeSettingsDialog()" reopens **Packages** picker. **OK** -> pause + progress + scan + popup + "ShowPackagesMenuChainSync()" |
| Progress | "UI.OptionSetting.ScanPackages.Progress" -> "{0.Number}%" |
| Result popup | **3 distinct outcomes** (see STBL table below) |

| Condition | STBL key | Content |
|-----------|----------|---------|
| "PackagesAdded > 0" | ".Complete" | "{0}" packages, "{1}" stages, "{2}" valid found + **code-appended** "AddedNames" list |
| "PackagesAdded == 0" && "ValidPackagesFound > 0" | ".None" | No new packages + **already registered** list ("AlreadyRegisteredNames") |
| "PackagesAdded == 0" && "ValidPackagesFound == 0" | ".NoneNoValid" | No valid WooHoo pack + "{0.Number}" = **"ModFilesScanned"** (XML group-0 candidates examined) |

**"PackageScanResult" fields:** "AddedNames", "AlreadyRegisteredNames" (dedupe case-insensitive: "alreadyRegistered" + "alreadyLoaded"), "ModFilesScanned", "ValidPackagesFound".

**STBL semantics:** In ".None", "{0.Number}" is **"ModFilesScanned"** (~8903 "_XML" group-0 candidates in KeySearch fallback), not the count of packages listed in the popup body.

### Discovery architecture (critical)

| API | Enumerates | Useful for anim packs? |
|-----|------------|------------------------|
| "GameUtils.GetModFilesCount" / "GetModFilesName" | Script mods (S3SA/DLL) only | **No** -> do not use for animation scan |
| **"KeySearch(53690476)"** + "Simulator.LoadFromResourceKey" | All mounted "_XML" in merged DB | **Yes** -> **production fallback** (~8903 resources) |
| "ContentManager.GetFirstPackageId" + "GetPackageName" | Native CC package registry | **Yes** (performance candidate) -> **empty in-world** during Settings scan |
| "ContentManager.GetFirstFilename" | Disk path | **Empty in-world** |

**KW animation XML:** type "53690476" ("0x0333406C", "_XML"), group "0", instance = FNV64 of logical resource name.

### Scan pipeline ("WooHooStage.cs")

1. "LogModsFilesystemTier3Probe()" -> **diagnostic only** (no candidate change).
2. "ResetPackageNameResolutionIndex()".
3. "EnsurePackageDiskResourceIndex()" -> "GetFirstFilename" -> fallback "GetFirstPackageId" (both empty in-world -> KeySearch).
4. "CollectPackageScanResourceKeys()" -> disk DBPF index if built, else **KeySearch** full merge.
5. Per candidate: skip registered / non-candidate / already loaded -> else "ProbeAndLoadPackageForScan".
6. "PostLoad()" if new packages added.
7. "WritePackageScanTimingDiagnostics" + INFO summary.

**Candidate filters ("IsScanCandidateResourceKey"):** exclude **"KinkyStages"** core; require **"DownloadContent.IsCustomContent"**; root **"WooHooStages"** + at least one **"WooHooStage"**.

**Package name resolution (cascade):** registered settings match -> static rubric "sKnownWooHooPackageNames" -> filename basename index -> disk DBPF index -> "GetLocalKeyFromSourceKey" + "GetPackageName" -> **prefix inference** from first stage "<Key>" (before ".", lowercase) -> hex fallback "0x" + 16-digit instance.

### Performance (known limitation)

- In-world disk index fails: "rootCause=GetFirstFilenameAndPackageIdEmpty" -> **KeySearch fallback** -> scan **~5–10 min** acceptable with confirm dialog.
- Example timing log: "totalMs≈316343", "collectKeysMs≈15", "mainLoopMs≈316281", "isCustomContentChecks≈8897", "xmlValidationLoads≈6080", "probeAndLoadCalls=4".
- Bottleneck: main loop ("IsCustomContent" + XML validation per candidate), not KeySearch collect.
- **Future:** tier 3 Mods filesystem walk (after **mods path** hardening), disk index fast path, XML validation cache / eliminate double load ("doubleXmlLoadCandidates").

**Tier 3 (prototype):** walk "Mods/Packages" + "Overrides" (depth 5), DBPF parse -> **log-only** in production; "modsRoot=unresolved" in-world today.

### Diagnostics

- Active during scan only ("__packageScanDiagnostics").
- Prefix **"### SCAN ###"** -> "KWErrorLog_*" when **Enable Global Buffer** + **"LogKWErrorOnQuit"** ON.
- Includes: "packageScanTiming", "packageDiskIndex", "packageScanSource", "packageNameInferred", "packageSkipped" / "packageRejected", closing **"### INFO ### : WooHooStage.ScanModFilesForPackages"**.

### Issues fixed (summary)

| Issue | Fix |
|-------|-----|
| False positives from stray XML | "WooHooStages" + "IsCustomContent" + skip KinkyStages |
| Illegible "0x" + decimal names | Safe "Tools.Parse" + name cascade + **prefix inference** |
| Opaque scan / accidental long run | "### SCAN ###" logs + **pre-scan confirm** |
| Single popup for “already present” vs “none valid” | Split **".None"** / **".NoneNoValid"** + name lists |
| Experimental Scan 2 | Removed |

### Changes (files)

| File | Role |
|------|------|
| "Oniki.Gameplay/WooHooStage.cs" | Scan, name resolution, disk index, tier-3 probe, timing |
| "Oniki.UI/OptionSettingScanPackages.cs" | Confirm, progress, 3 popups, lists, menu reopen |
| "Oniki.UI/OptionSettingMenuPackages.cs" | Menu entry |
| "Oniki.UI/OptionSettings.cs" | "ShowPackagesMenuChainSync()" |
| "Oniki.Utilities/Tools.cs" | Safe "Parse" for "0x" keys |

**Removed:** "OptionSettingScanPackages2.cs", "PackageScanSourceMode", dual-mode scan overload.

### STBL (STBL)

Runtime prefix: "Oniki.KinkyMod." + logical key.

| Logical key | Role |
|-------------|------|
| "UI.OptionSetting.ScanPackages" | Menu label |
| "UI.OptionSetting.ScanPackages.Confirm" | Pre-scan confirm |
| "UI.OptionSetting.ScanPackages.Title" | Result popup title |
| "UI.OptionSetting.ScanPackages.Progress" | Progress "{0.Number}%" |
| "UI.OptionSetting.ScanPackages.Complete" | Success + appended "AddedNames" |
| "UI.OptionSetting.ScanPackages.None" | Already registered + appended list |
| "UI.OptionSetting.ScanPackages.NoneNoValid" | No valid packs |

*(Hashes TBD in package pass.)*

**EN code fallbacks** (if STBL missing): confirm text warns **several minutes**; "NoneNoValid" references "{0.Number} scanned file"; "None" references "{0.Number} mod file(s) scanned" + “Already registered resources:” header.

---

## Consensual rough kink: Masochist / Submissive / Slut differentiation (Build 450 wave)

### Design intent

Separate **consensual rough/BDSM** rewards from shared "GoodSpank" and from **mean violence** (grope/spank discipline/rape) -> **excluded** from Violence patch when interactions are consensual; see sections below.

| Trait | Target role | Shipped reward (450) |
|-------|-------------|----------------------|
| **Masochist** | enjoys pain/rough | "GoodSpank" (spank only) + **"MasochistRoughBliss"** (WooHoo stacks) |
| **Submissive** | consensual submission | **"PleasureTrance"** + presence **"SubmissiveFeeling"** (see **Dominant/Submissive upgrade** below); interim: "CompletedLiked" + horny on spank without D+S pair |
| **Slut** | sexual/exhibition kink | "CompletedLiked" + horny on spank (no "GoodSpank"); **"SlutBliss"** not implemented |

**Violence patch:** Insulting LTR/"--" on **non-consensual** grope/spank/rape witness paths; consensual spank / ask rough / consensual WooHoo rough **excluded** -> see **Violence patch** section below ("ConsensualRoughTools" = future backlog).

---

### Pilastro A -> Consensual spank (Fase 1a -> shipped)

| Change | Detail |
|--------|--------|
| **"GoodSpank"** | Consensual spank -> **Masochist target only** ("Spank.PostSocialLoop") |
| **"AskToSpank" acceptance** | Targets with **Submissive**, **Masochist** / **MasochistReward**, or **Slut**: Awkward **5** / Accepted **20** (vs 20/40); **no** harsh-actor penalty (Dominant/Evil) |
| **Interim Sub/Slut** | No "GoodSpank"; keep **"CompletedLiked"**, **"SetHorny()"**, **"DominantSatisfaction"** when Dominant->Submissive pair |


**Files:** "Oniki.Interactions/AskToSpank.cs", "Oniki.Gameplay.Punishments/Spank.cs".

---

### Pilastro B -> Ask for rough sex (Fase 3 -> shipped)

| Field | Value |
|-------|--------|
| Interaction | "WooHooSequence.Definition_Rough" / "Singleton_Rough" (+ **"Definition_Incest_Rough"** for blood-related when incest ON) |
| Visibility | **Masochist on actor or target** (symmetric with AskToSpank pattern) |
| Picker | Stages with "IsConsensualRoughKinkStage" -> "(Rough \|\| BDSM) && !Rape" (includes **legacy Key inference**, Fase 2c) |
| WooHoo | "IsRape = false"; no violence victim LTR from this path |
| Refinements | Mood stack **+10** per stack (not +5); **no** "EnableAskForRoughSex" toggle -> always on when visible |

**STBL:**

- "Oniki.KinkyMod.AskForRoughSex:InteractionName"
- "0x151912AEB7EC85C7"
- "EN: Ask for rough sex" *(confirm in package)*

**Files:** "Oniki.Interactions/WooHooSequence.cs".

---

### Pilastro C -> "MasochistRoughBliss" / “Sado Bliss” (Fase 2 -> shipped)

| Field | Value |
|-------|--------|
| GUID / codename | "MasochistRoughBliss" -> "0xB29F84B90579BD36" |
| Class | "BuffMasochistRoughBliss" + "BuffCreampiedInstance" stacks |
| Origin | "Origins.FromMasochistRoughBliss" = "0x933650ABA4D3D3A6" |
| Mood | **+10** per stack ("kMoodPerStack"); cap **10** stacks; timeout **240** sim-min |
| Thumb | "moodlet_masochist_bliss" (XML + code fallback) |

**Apply rules:**

- Successful WooHoo ("mSuccess"), "!IsSolo", "!IsRape", stage not **Pose**
- Stage: "QualifiesForMasochistRoughBliss" -> "(Rough \|\| BDSM \|\| Category Anal) && !Rape" (Rough/BDSM include **Key token inference**)
- **Masochist participants only**; not on spank path; dedupe **one stack per stage key** per session
- Hooks: "WooHooInstance.EndStage()" + "Stop()"; stack via manual increment when buff already present (fix **v3** -> no "AddElement" re-add spam / ScriptError export)

**STBL:**

| Key | Hash | EN |
|-----|------|-----|
| "Gameplay/Excel/buffs/BuffList:MasochistRoughBliss" | "0x1AFA4F457DF3DEEB" | Sado Bliss |
| "Gameplay/Excel/buffs/BuffList:MasochistRoughBlissDescription" | "0xEB5841FD73579D0F" | Description with "x{2.Number}" stack prefix |
| "Gameplay/Excel/buffs/BuffOrigins:933650ABA4D3D3A6" | *(FNV64 from origin key)* | "(From Having Rough Sex)" |

**Diagnostics:** "[MasochistRoughBliss]" -> **"General_Logging_*.xml"** when **Miscellaneous logging** + global buffer ON (not "KWInteractionLog_*"; no "Log.Write(exception)" -> see logging refactor section above).


**Files:** "BuffMasochistRoughBliss.cs", "MasochistRoughBlissTools.cs", "KWBuff.cs", "Origins.cs", "WooHooInstance.cs", "WooHooStage.cs", "KinkyBuffs" XML.

---

### Pilastro C2 -> Legacy stage Key "rough" / "bdsm" (Fase 2c -> shipped)

Animation packs without "<Flags>Rough</Flags>" / "<Flags>BDSM</Flags>" (e.g. **ooOLalaCity** "KW_OLLCity_Anims") still count as rough/BDSM when **Key** contains delimited token "rough" or "bdsm".

**Guards (no inference):** "Rape" flag, key on **"WooHooRapeList"**, **Hidden** stage. **Rape "Find(..., Rough flag)"** remains XML-flag-only (by design).

**Propagates to:** bliss stacks, ask-rough picker, womb/anal feedback gate, "AcceptWooHoo" / "WooHooJoin" masochist bonus.

**File:** "WooHooStage.cs" ("HasRoughOrBdsmSemantics", "IsConsensualRoughKinkStage", property "Rough" / "BDSM"); "WooHooTools.cs", "WooHooJoin.cs".


---

### Pilastro C3 -> Consensual rough womb/anal cum-inside dialog (Fase 2b)

**Module:** "ConsensualRoughWombFeedbackTools.cs" -> same gate as bliss; **masochist** womb holder / anal target; **"!rape"**.

| Behavior | Detail |
|----------|--------|
| Vaginal | "PostWooHoo_Vaginal" -> "Womb.AddSperm(..., consensualRoughKinkFeedback)" |
| All "!rape" womb slots | "TryShowWombCumInsideDialog" first -> else **vanilla** CoinFlip (**no** legacy ":masochist" on non-rough) |
| Anal | "PostWooHoo_Anal" -> "TryShowAnalCumInsideDialog" |
| **Rape + masochist** | Legacy "*Rape*:masochist" paths **unchanged** |

**Legacy cleanup:** removed Grandma/Mommy "!rape" -> ":masochist" fallback (fixed “masochist flavor” on vanilla consensual incest WooHoo).

**STBL:** **28 new keys** "Oniki.KinkyMod.WooHoo/Feedback:*:masochist" (26 womb + 2 anal).

Full key list: **28** entries -> "WooHoo/Feedback:CumInsideWomb(Fertile|NonFertile)*:masochist" (26 womb slots/bands) + "CumInsideAnal(:masochist|2:masochist)" (2 anal).

**Files:** "ConsensualRoughWombFeedbackTools.cs", "Womb.cs", "WooHooTools.cs", "Kraken.cs" (feedback flag "false").

---

### Collateral -> WooHoo stage picker & settings (same wave)

| Item | Detail |
|------|--------|
| **"UseMultiCategoryStagePicker"** | Default **"false"** -> legacy mono-tab on category asks (Oral = oral only); **ON** or "WooHooTypes.All" -> multi-tab. Rough ask uses "All" -> multi-tab even when OFF. Migration "PreviousVersion < 450". |
| **Tab snap-back fix** | "WooHooProxy.cs" -> one-shot "initialTabApplied" on picker open |
| **Menu order** | "DurationSpank" moved under "EnableConsensualSpanking" in General Gameplay |

**STBL:** "Oniki.KinkyMod.OptionSettings.UseMultiCategoryStagePicker" -> *(hash TBD)* -> "EN: Use multi-category stage picker"

---

## Dominant / Submissive trait upgrade (Pleasure Trance + presence buffs)

### Overview

Extends the legacy **"DominantSatisfaction"** pattern with a **symmetric Submissive reward**, **rebalanced pair moodlets**, **consensual spank hygiene**, and **indefinite presence buffs** when complementary traits share a room (or same-lot fallback).

**No settings toggles** -> baseline Dominant/Submissive module behavior.

**Related:** **Masochist / Slut / rough consensual** wave (separate from Dominant/Submissive). **Slut** dedicated buff not implemented in this build.

---

### Moodlet matrix (shipped in code)

| Buff | Recipient | Trigger | Duration | Mood |
|------|-----------|---------|----------|------|
| **"DominantSatisfaction"** (rebalanced) | **Dominant** | Consensual spank or WooHoo with **Submissive** partner | **180 sim-min** | **+80** Happy |
| **"PleasureTrance"** (new) | **Submissive** | Same D+S pair events (mirror) | **180 sim-min** | **+80** Happy |
| **"DominantStrength"** | **Dominant** | **Submissive** in same room (or ≤**10** tiles same lot) | **Indefinite** ("TimeoutLength -1") | **+15** Happy |
| **"SubmissiveFeeling"** | **Submissive** | **Dominant** nearby (symmetric) | **Indefinite** | **+15** Happy |

**Pair rewards:** one-shot, **not stackable** (shared constants "kDominantSubmissivePairRewardMood" / "kDominantSubmissivePairRewardTimeoutSimMinutes" in "KWBuff.cs"). Coexists with **"GoodSpank"** on Masochist+Submissive targets.

**Presence:** throttle **every 6 ticks** + **immediate** sync on "RoomId"/"LotId" change; sleeping Sims count; rabbit hole / off-world -> buff removed next tick; **"Travel: True"** on XML.

---

### Phase A -> Pleasure Trance + spank hygiene (code shipped)

**"DominantSatisfaction" XML:** "EffectValue" **80**, timeout **180** ("0x4D892C8B190A8C31", thumb "moodlet_dominant_satisfaction").

**"PleasureTrance" XML:** "0xFCC71FE67E5E073B", thumb "moodlet_pleasure_trance", "Stackable=False", "Travel=True".

**Grant paths:**

| Event | Dominant gets | Submissive gets |
|-------|---------------|-----------------|
| Consensual spank ("Spank.PostSocialLoop") | "DominantSatisfaction" ("FromDominantSpanking") | "PleasureTrance" ("FromSubmissiveSpanking") |
| Consensual WooHoo ("WooHooInstance.EvaluateOutcome") | "DominantSatisfaction" ("FromDominantWooHoo") -> once/session | "PleasureTrance" ("FromSubmissiveWooHoo") -> once/session |

Gate: D+S roles, "!Rejected", consensual; WooHoo also requires non-rape, not interrupted, "Category >= Teasing", buff not already present.

**SpankRough hygiene:** legacy "force" branch (Insulting + "SpankRough"/"Sore") runs **only when "!isConsensual"** -> fixes Dominant-on-target wrongly applying discipline moodlets on **ask-to** consensual spank.

**"AskToSpank" acceptance (extended):**

- Auto-accept: actor **Dominant** OR target **Submissive** in domination relation ("DominationEnabled" + "IsSubmissive")
- Exhibition gate: difficulty **or** target **aroused** ("IsAroused")
- Kink-likable thresholds unchanged (Sub/Maso/Slut: 5 / 20)
- **Not** mirrored: Willpowermax auto-accept from "AskToBeSpanked"

**STBL (STBL):**

| Key | Hash | EN |
|-----|------|-----|
| "Gameplay/Excel/buffs/BuffList:PleasureTrance" | "0x21AA9A17620793E8" | Pleasure Trance |
| "Gameplay/Excel/buffs/BuffList:PleasureTranceDescription" | "0x9F16D70BF7AB39E2" | *(description in package)* |
| "Gameplay/Excel/buffs/BuffOrigins:74EA8374CF07A622" | "0x03A3041BB684115B" | (From Submissive Spanking) |
| "Gameplay/Excel/buffs/BuffOrigins:292F4228C8A679C2" | "0xDCFF69092377F01F" | (From Submissive WooHoo) |

**DominantSatisfaction origins (existing / updated balance):** "FromDominantSpanking" "2F4055FE9DAE19D6", "FromDominantWooHoo" "3484C673B396DB6" -> deploy STBL if balance text changed.

---

### Phase B -> Dominant Strength / Submissive Feeling (code shipped)

**Architecture:** "DominantSubmissivePresenceTools.cs" -> **not** "BuffAura" / radius broadcaster (those affect *other* Sims).

| API | Behavior |
|-----|----------|
| "HasComplementaryTraitNearby" | Scan "lot.GetSims()" with early-exit; primary **same "RoomId"**; fallback distance ≤ "LotTools.kDistanceToReact" (**10** tiles) |
| "SyncPresenceBuffs" | Ensure +15 / "-1f" buff or remove; no re-add refresh when already correct |
| "RemovePresenceBuffs" | On leave world / failed sync |

**Hook:** "SimData.UpdateDominantSubmissivePresenceBuffs" when "InWorld" (6-tick throttle + room/lot fast-path).

**XML:**

| Buff | Hex | Effect | Timeout |
|------|-----|--------|---------|
| "DominantStrength" | "0x1A56C198DDA381B4" | +15 | -1 |
| "SubmissiveFeeling" | "0x5DADE3B064755903" | +15 | -1 |

Thumbs: "moodlet_dominant_strength", "moodlet_submissive_feeling".

**STBL:**

- "Gameplay/Excel/buffs/BuffList:DominantStrength" -> "0xFED48B9223358267"
- "DominantStrengthDescription", "SubmissiveFeeling", "SubmissiveFeelingDescription" -> *(hashes TBD in package pass)*

**Deferred v1:** mood stack when multiple D/S in same room (still flat +15).

---

### Changes (files)

| File | Role |
|------|------|
| "Oniki.Gameplay/KWBuff.cs" | Enum + pair constants + patch |
| "Oniki.Gameplay/Origins.cs" | Submissive origins |
| "Oniki.Gameplay.Punishments/Spank.cs" | Trance + Satisfaction + "!isConsensual" SpankRough gate |
| "Oniki.Gameplay/WooHooInstance.cs" | "ShouldGrantPleasureTrance" + grant |
| "Oniki.Interactions/AskToSpank.cs" | D+S auto-accept + exhibition/arousal |
| "Oniki.Gameplay/DominantSubmissivePresenceTools.cs" | Presence sync (**new**) |
| "Oniki.Gameplay/SimData.cs" | Presence tick hook |
| "KinkyBuffs%%+_XML.xml" (+ "ONIKI_KinkySettings" mirror) | Four buff entries |
| "Oniki_KinkyMod.csproj" | Compile include |

---

## Violence patch: LTR, Insulting VFX ("--"), witness panic (Build 450)

### Overview

Unified **relationship fallout** and **witness reactions** for KW violent social interactions: **Grope**, **PullDownClothes** (forced/mean path), **non-consensual Spank**, and **Rape LTR/VFX** (Option A -> numbers aligned, **no** rewrite of "ReactToRapist" police/fight/coward).

**Core helper:** "Oniki.Gameplay/ViolenceSocialTools.cs" + "Oniki.Situations/ReactToViolence.cs".

**Consensual rough kink** (spank ask-to, ask rough sex, consensual WooHoo BDSM) **excluded** from this pipeline -> see **Masochist / Dominant** sections above.

**Default (450):** modern violence pipeline above. **Optional 442 restore:** **"Legacy Violence Toggles"** section below ("EnableLegacy*" gates + Violence settings hub).

---

### Baseline constants (Grope-named, reused everywhere)

| Constant | Value | Role |
|----------|-------|------|
| Witness LTR | **-35** | Witness -> aggressor |
| Victim LTR | **-50** | Victim -> aggressor |
| Witness STC Insulting | **+100** | Short-term commodity |
| Victim STC Insulting | **+150** | Short-term commodity |
| VFX | EA **"SetSocialFeedback(Insulting, …)"** | Visible **"--"** on relationship panel |

---

### Witness session (Grope + non-consensual Spank)

| API | Use |
|-----|-----|
| "BeginGropeWitnessSession" / "EndGropeWitnessSession" | Sustained ~2 sim-min broadcaster pulse |
| "ApplyGropeVictimFallout" | Victim LTR/STC/VFX at end of sync |
| "TryQueueViolenceWitnessReaction" | Queue **"ReactToViolence"** on witnesses |

**Per witness per session:** LTR one-shot once ("ProcessedWitnessIds").

**Queue behavior (post-fix):**

- "InteractionPriorityLevel.Fire" + **"SimTools.InsertNextInteraction(mustRun: true)"**
- **"Hidden = false"**; interrupt current via **"ExitReason.CanceledByScript"**
- **"StripWitnessAutonomyTail"** -> cancels autonomous tail (fixes post-panic **GoHere** resume)
- **8 sim-min cooldown removed** (witness can re-react during same session)
- Pre-clean: cancel "EnterRelaxing" on sleeping witnesses where applicable

**Diagnostics:** "[ViolenceWitness]" -> **"General_Logging_*.xml"** when **Debug interaction logging** + global buffer ON.

**Fight on grope/spank:** "TryPushFightAgainstAggressor" may queue KW **"Fight"**, but **"Fight" requires "ICanBeArrested" (rape situation)** -> aggressor on grope/spank logs "noArrestableSituation=True" -> **panic is the real outcome** (by design until vanilla fight path added).

---

### "ReactToViolence" (witness behavior)

Clone of "ReactToRapist" **without** police / "RapeSituation".

**Flow:** posture prep -> strip autonomy tail -> route to aggressor -> best-effort fight queue -> **"RunPanicWhileSessionActive"** (EA "ReactToFire" panic loop) for **entire** active violence session -> **always**, even if "FightQueued" logged.

**Not in scope:** coward run-away / Scared buff on this path (rape uses "ReactToRapist").

---

### Per-interaction wiring

| Interaction | Violence pipeline |
|-------------|-------------------|
| **Grope** | Full session Begin/End + victim fallout at exit |
| **Spank** non-consensual | Same session as Grope when "ShouldApplySpankViolenceFallout" |
| **Spank** consensual ("AskToSpank" / "AskToBeSpanked") | **Excluded**; "CompletedLiked" **only** if "isConsensual" |
| **PullDown** forced | One-shot witness + victim fallout; **no** sustained session |
| **PullDown** consensual | Legacy exhibition -> no "ViolenceSocialTools" |
| **Rape** witnesses | "RapeBroadcaster.TryApplyRapeWitnessFallout" replaces legacy **-25** flat |
| **Rape** victim | "ApplyRapeVictimFalloutIfNeeded" in "PostWooHooRape" |

**Spank fallout gate ("ShouldApplySpankViolenceFallout"):**

"""text
false -> consensual OR mod disabled OR EnableLegacySpankLogic ON
true  -> any non-consensual spank (disciplinary, perfidious/EvilSpank, trait force, headmaster, …)
"""

**Note:** **"EvilSpank"** setting only affects **pie menu Test()** visibility -> **not** violence fallout eligibility.

**"CompletedLiked" fix:** appreciation STBL only when **"isConsensual"** -> suppressed on perfidious/discipline paths that already get insulting victim fallout.

---

### PullDown forced path (mean assault)

**Gate:** "IsForcedAssault() = !ConsensualContinuation && EnablePullDownClothes && !EnableLegacyPullDownLogic"

| Step | Behavior |
|------|----------|
| Wake + fallout | "ApplyGropeWitnessFallout" + "ApplyGropeVictimFallout" (one-shot) |
| Get dressed | "MarkPendingForcedGetDressed"; defer poll; **"SkipWakeRelaxing"** chain |
| EnterRelaxing mitigation | Multi-point cancel (PullDown, defer tick, "GetDressed.Run", "KWBedSleep" guard, removed global **Relaxing** precondition on GetDressed) |

**Accepted behavior:** **EnterRelaxing** may still **enqueue** post-wake but is **cancelled** when GetDressed becomes current -> partial queue appearance OK.

**STBL:** "Oniki.KinkyMod.GetDressed.InteractionFeedback:Gropped" -> "{0}" victim, "{1}" aggressor.

---

### Rape -> Option A (LTR/VFX only)

**Policy:** align **-35 / -50** + Insulting STC/VFX with Grope; **do not** change "ReactToRapist" police/fight/coward/masochist-ignore timing.

| Gate | Default | Effect |
|------|---------|--------|
| "rapeHurtSimRelationship" | ON | Witness LTR/VFX |
| "rapeHurtVictimRelationship" | ON | Victim fallout end of rape |

**Witnesses ("RapeBroadcaster"):**

- Legacy "UpdateLiking(-25)" **removed** -> "ApplyRapeWitnessFallout"
- LTR/"--" applied **before** masochist/submissive ignore dialogs (witness still gets penalty)
- “Bad” witnesses who could rape ("CanRape") **excluded** -> confirmed OK
- Queue cooldown on "ReactToRapist" **removed** (was 8 min via "OffCooldown" check)
- **Still uses** "ReactToRapist" for behavior -> **not** "ReactToViolence"; **no** "[ViolenceWitness]" log on rape

**Victim ("PostWooHooRape"):**

- Non-masochist with legacy Steamed **-100** + "InteractionBits.Rape" -> **"ApplyRapeVictimInsultingVfxOnly"** (no double LTR)
- Masochist (skips Steamed) -> full **"ApplyRapeVictimFallout"** (-50 + "--")

**Police absence on 1st rape (analyzed log "CAA1382E"):** masochist victim + sleeping witnesses + "IamMasochist" ignore -> **legacy**, not regression from this patch. **No police fix** in 450.

---

---

### Changes (files)

| File | Role |
|------|------|
| "ViolenceSocialTools.cs" | LTR/STC/VFX; session; queue; "ApplyRape*"; spank gate |
| "ReactToViolence.cs" | Panic loop; fight attempt; no cooldown |
| "Grope.cs" | Session Begin/End |
| "Spank.cs" | Non-consensual session; "CompletedLiked" consensual-only |
| "PullDownClothes.cs" | Forced fallout + wake/GetDressed chain |
| "GetDressed.cs" | Gropped STBL; skip relaxing; cancel EnterRelaxing |
| "KWBedSleep.cs" | "ShouldSkipWakeRelaxingForVictim" |
| "RapeBroadcaster.cs" | Witness fallout Option A |
| "WooHooInstance.cs" | Victim fallout Option A |

---

---

## Legacy Violence toggles + Violence settings hub (442 compatibility)

### Overview

Build **450** ships the **Violence patch** (LTR/"--", witness panic) as the **default** when legacy gates are OFF. This wave adds:

1. **Settings UI hub** -> **Violence** top-level menu with per-feature submenus (Grope, Rape, Evil Spank, Pull Down).
2. **Optional "EnableLegacy*" toggles** -> restore **Build 442**-style behavior without removing 450 code paths.

**Does not replace** the Violence patch -> legacy ON **disables** or **bypasses** specific 450 fallout/acceptance paths via gates documented below.

**Out of scope:** mod-vs-mod compatibility (NRaas WooHooer, etc.) -> not part of this release wave.


---

### Settings UI hierarchy

"""text
Violence                    ← hub (OptionSettingMenuRape -> STBL MenuViolence.Label)
├─ Rape                     ← rape settings + RapeService (moved from Services)
│    ├─ Enable Rape, genders, control sequence, call police
│    ├─ Victim fight win bonus (0–100%)
│    ├─ Masochists and submissives can fight (autonomous rape only)
│    └─ Services: Rapists
├─ Grope                    ← Enable Grope + Restore legacy grope logic
├─ Evil Spank               ← Evil Spank + Use old logic for spanking
└─ Pull Down Clothes        ← Enable Mean Pull Down + Use old logic for pull down
"""

**Rape fight note:** masochist/submissive **skip pre-fight** applies to **situation/service rape** only -> **player-directed** rape pie menu still offers fight.

---

### New / migrated settings keys

| Key | Default | UI location | Role |
|-----|---------|-------------|------|
| "EnableGrope" | "false" | Grope | Master grope switch (migrated from "Rapes" if missing on upgrade) |
| "EnableLegacyGropeAcceptance" | "false" | Grope | 442 acceptance path; **no** violence helper |
| "EnableLegacySpankLogic" | "false" | Evil Spank | Disables violence fallout on **all** non-consensual spank |
| "EnablePullDownClothes" | "false" | Pull Down | Mean/master 450 -> Insulting pie, forced wake + violence |
| "EnableLegacyPullDownLogic" | "false" | Pull Down | Full 442 pull-down (Kinky pie, exhibition; sleep no wake) |
| "MasochistsAndSubmissivesCanFight" | "false" | Rape | Situation-path fight skip for maso/sub victims |
| "RapeVictimFightWinBonus" | "0" | Rape | % chance victim wins pre-fight roll (**bug fix:** "mLoser = victimWins" was inverted) |

**Repositioned (unchanged keys):** "Rapes", "EvilSpank" under Violence submenus (no longer gate grope globally).

**Migration:** "PreviousVersion < 450" -> "EnableGrope" seeded from "Rapes" if absent; all legacy keys add-if-missing "false". Travel: follows standard "Settings" clone (Build 449+ policy).

---

### Behavior matrix (450 vs legacy)

**Grope**

| Enable Grope | Legacy grope | Acceptance | Violence patch |
|--------------|--------------|------------|----------------|
| OFF | -> | Hidden | -> |
| ON | OFF | Modern 450 | Full session + LTR/"--" |
| ON | ON | **442 "GetResultLegacy442()"** | **Off** (no "-5" LTR ×2 reintroduced) |

**Spank (non-consensual)**

| Legacy spank logic | Violence fallout | "CompletedLiked" |
|------------------|------------------|------------------|
| OFF | Yes (perfido + disciplinary) | Only if consensual Ask* |
| ON | **No** on all non-consensual | Only if consensual Ask* |

**Pull Down**

| Mean ON | Legacy ON | Pie / path |
|---------|-----------|------------|
| OFF | OFF | Feature unavailable |
| ON | OFF | **Mean 450** -> forced wake, violence, Angry GetDressed |
| OFF | ON | **442** -> Kinky pie, exhibition accept/reject; sleep without forced assault |
| ON | ON | **Legacy wins** over mean |

Helpers ("PullDownClothes.cs"):

- "IsPullDownFeatureAvailable()" = legacy OR mean
- "IsMeanPullDownEnabled()" = mean ON **and** legacy OFF
- "IsForcedAssault()" = "!ConsensualContinuation && IsMeanPullDownEnabled()"
- "SocialCallbacks" retaliation after reject: only when mean enabled

**Rape LTR/VFX (Violence patch Option A):** **not** gated by legacy toggles in this wave -> "rapeHurtSimRelationship" / "rapeHurtVictimRelationship" remain independent.

---

### Interaction with Violence patch (quick reference)

| Feature | 450 default (legacy OFF) | Legacy ON effect |
|---------|--------------------------|------------------|
| Grope | "BeginGropeWitnessSession" + victim fallout | No violence helper; 442 accept |
| Spank non-consensual | "ShouldApplySpankViolenceFallout" true | Gate false -> no session/LTR |
| Pull Down sleep assault | Forced fallout + wake chain | Exhibition 442 only |
| Rape witness/victim LTR | "ApplyRape*" pipeline | Unchanged |

---

### Changes (files)

| File | Role |
|------|------|
| "OptionSettingMenuRape.cs" | Violence hub (class name legacy; STBL "MenuViolence.Label") |
| "OptionSettingMenuViolenceRape.cs" | **New** -> rape submenu |
| "OptionSettingMenuViolenceGrope.cs" | **New** |
| "OptionSettingMenuViolenceEvilSpank.cs" | **New** |
| "OptionSettingMenuViolencePullDown.cs" | **New** |
| "OptionSettingRapeVictimFightWinBonus.cs" | **New** slider |
| "OptionSettingMenuServices.cs" | RapeService removed from Services |
| "Settings.cs" | Keys + migration |
| "Grope.cs" | Dual GetResult + local violence gate |
| "PullDownClothes.cs" | Mean/legacy helpers |
| "ViolenceSocialTools.cs" | "ShouldApplySpankViolenceFallout" respects legacy |
| "Spank.cs" | "SetHorny()" consensual-only when legacy |
| "SocialCallbacks.cs" | Mean pull-down retaliation gate |
| "Rape.cs" / "RapeSituationBase.cs" | Fight bonus + maso/sub skip |

---

### STBL (minimum 11 new menu keys)

| Full key | EN (proposed) |
|----------|---------------|
| "Oniki.KinkyMod.OptionSettings.MenuViolence.Label" | Violence |
| "Oniki.KinkyMod.OptionSettings.MenuViolence.Rape" | Rape |
| "Oniki.KinkyMod.OptionSettings.MenuViolence.Grope" | Grope |
| "Oniki.KinkyMod.OptionSettings.MenuViolence.EvilSpank" | Evil Spank |
| "Oniki.KinkyMod.OptionSettings.MenuViolence.PullDown" | Pull Down Clothes |
| "Oniki.KinkyMod.OptionSettings.EnableGrope" | Enable grope interactions |
| "Oniki.KinkyMod.OptionSettings.EnableLegacyGropeAcceptance" | Restore legacy grope logic (442 acceptance, no witness violence) |
| "Oniki.KinkyMod.OptionSettings.EnableLegacySpankLogic" | Use old logic for spanking |
| "Oniki.KinkyMod.OptionSettings.EnableLegacyPullDownLogic" | Use old logic for pull down |
| "Oniki.KinkyMod.OptionSettings.RapeVictimFightWinBonus" | Victim fight win bonus (%) |
| "Oniki.KinkyMod.OptionSettings.MasochistsAndSubmissivesCanFight" | Masochists and submissives can fight back (autonomous rape) |

*(Hashes TBD in package pass.)*  
**Copy review:** existing "EnablePullDownClothes" STBL -> suggest **Enable Mean Pull Down Clothes**.  
**No new dialogue STBL** in this wave -> reuses existing interaction strings.

---

---

## Version metadata (reference)

Source: "Oniki/KinkyMod.cs" (active "Project" tree).

| Field | Value | Role |
|-------|-------|------|
| "sVersion" | "0" | Major |
| "sSubVersion" | "9" | Minor (the **9** in **0.9**.x) |
| "sRevision" | "5" | Patch (the last **5** in **0.9.5**) |
| "sBuild" | "450" | Internal build counter (popup + save migration) |

**In-game popup / log header:** "Oniki's Kinky World: v0.9.5 Build 450" (built from the three version bytes + "sBuild").

**Serialized "Version" int:** "(sVersion << 16) \| (sSubVersion << 9) \| sRevision" -> used for save upgrade detection vs stored settings build.